From d85e1b5807529206475d56f190096c99e15b4092 Mon Sep 17 00:00:00 2001 From: Adam Macdonald <72780006+twokilohertz@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:45:06 +0000 Subject: [PATCH] Drawing CO2 graph --- Cargo.lock | 19 +++++++- Cargo.toml | 2 + src/display_task.rs | 104 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 111 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc07f82..71b0c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "circular-buffer" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -697,6 +703,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "lalrpop" version = "0.19.12" @@ -708,7 +723,7 @@ dependencies = [ "diff", "ena", "is-terminal", - "itertools", + "itertools 0.10.5", "lalrpop-util", "petgraph", "regex", @@ -880,6 +895,7 @@ dependencies = [ name = "pico-enviro-sensor" version = "0.1.0" dependencies = [ + "circular-buffer", "cortex-m-rt", "display-interface-spi", "embassy-embedded-hal", @@ -890,6 +906,7 @@ dependencies = [ "embedded-graphics", "embedded-graphics-framebuf", "heapless", + "itertools 0.14.0", "rtt-target", "scd4x", "ssd1351", diff --git a/Cargo.toml b/Cargo.toml index dd6fd6f..56f6936 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ display-interface-spi = "0.5.0" # Extra heapless = "0.8.0" +circular-buffer = { version = "1.0.0", default-features = false } +itertools = { version = "0.14.0", default-features = false } [profile.dev] opt-level = "s" diff --git a/src/display_task.rs b/src/display_task.rs index 440bb9a..448752b 100644 --- a/src/display_task.rs +++ b/src/display_task.rs @@ -17,14 +17,25 @@ use embedded_graphics::{ draw_target::DrawTarget, mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::Rgb565, - prelude::{Point, RgbColor, Size, WebColors}, - primitives::Rectangle, + prelude::{Point, Primitive, RgbColor, Size, WebColors}, + primitives::{Line, PrimitiveStyle, Rectangle}, text::{Alignment, Text}, Drawable, }; +// Containers +use circular_buffer::CircularBuffer; +use heapless::String; +use itertools::Itertools; + use crate::{Spi0BusMutex, SENSOR_DATA_SIGNAL}; -use core::fmt::Write; +use core::{ + fmt::Write, + ops::{Add, Div, Sub}, +}; + +const DISPLAY_WIDTH: usize = 128; +const DISPLAY_HEIGHT: usize = 128; /// Output to the SSD1351 display #[embassy_executor::task] @@ -50,16 +61,23 @@ pub async fn display_output_task( display.init().unwrap(); // Framebuffer, prevents flickering on redraw - let buf = [Rgb565::BLACK; 128 * 128]; - let mut framebuf = FrameBuf::new(buf, 128, 128); + let buf = [Rgb565::BLACK; DISPLAY_WIDTH * DISPLAY_HEIGHT]; + let mut framebuf = FrameBuf::new(buf, DISPLAY_WIDTH, DISPLAY_HEIGHT); + // Text styles let co2_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_DARK_GREEN); let temp_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_ORANGE); let humidity_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_AQUA); - let mut co2_text_buf = heapless::String::<16>::new(); - let mut temp_text_buf = heapless::String::<16>::new(); - let mut humidity_text_buf = heapless::String::<16>::new(); + // Format string buffers + let mut co2_text_buf = String::<16>::new(); + let mut temp_text_buf = String::<16>::new(); + let mut humidity_text_buf = String::<16>::new(); + + // Ring buffer for storing past measurement data + let mut co2_samples = CircularBuffer::<60, u16>::new(); + let mut temp_samples = CircularBuffer::<60, f32>::new(); + let mut humidity_samples = CircularBuffer::<60, f32>::new(); loop { // Clear the framebuffer @@ -71,10 +89,67 @@ pub async fn display_output_task( humidity_text_buf.clear(); // Wait on sensor data & format into the buffers - let data = SENSOR_DATA_SIGNAL.wait().await; - write!(&mut co2_text_buf, "CO2: {} ppm", data.co2).unwrap(); - write!(&mut temp_text_buf, "Temp: {:.1} C", data.temperature).unwrap(); - write!(&mut humidity_text_buf, "RH: {:.1} %", data.humidity).unwrap(); + let sensor_data = SENSOR_DATA_SIGNAL.wait().await; + write!(&mut co2_text_buf, "CO2: {} ppm", sensor_data.co2).unwrap(); + write!(&mut temp_text_buf, "Temp: {:.1} C", sensor_data.temperature).unwrap(); + write!(&mut humidity_text_buf, "RH: {:.1} %", sensor_data.humidity).unwrap(); + + // Record samples + co2_samples.push_back(sensor_data.co2); + temp_samples.push_back(sensor_data.temperature); + humidity_samples.push_back(sensor_data.humidity); + + let co2_min = *co2_samples.iter().min().unwrap(); + let co2_max = *co2_samples.iter().max().unwrap(); + let _temp_min = *temp_samples + .iter() + .reduce(|a: &f32, b: &f32| if a.le(b) { a } else { b }) + .unwrap(); + let _temp_max = *temp_samples + .iter() + .reduce(|a: &f32, b: &f32| if a.ge(b) { a } else { b }) + .unwrap(); + let _humid_min = *humidity_samples + .iter() + .reduce(|a: &f32, b: &f32| if a.le(b) { a } else { b }) + .unwrap(); + let _humid_max = *humidity_samples + .iter() + .reduce(|a: &f32, b: &f32| if a.ge(b) { a } else { b }) + .unwrap(); + + /* + Note about drawing positions: + The embedded-graphics library follows the OpenGL convention of the + top-left of the image being (0, 0) with X increasing to the right + and Y increasing downwards. + */ + + // Draw line graphs + + if co2_samples.len() >= 2 { + // CO2 line graph happens 4 pixels from the top, with 4 pixels either side + + let mut x_pos: i32 = (DISPLAY_WIDTH - 4) as i32; + + for (a, b) in co2_samples.iter().rev().tuple_windows::<(_, _)>() { + let co2_range = if (co2_max - co2_min) == 0 { + 1 + } else { + co2_max - co2_min + }; + + let a_y_pos: i32 = (4 + (((co2_max - a) * 30) / co2_range)) as i32; + let b_y_pos: i32 = (4 + (((co2_max - b) * 30) / co2_range)) as i32; + + Line::new(Point::new(x_pos, a_y_pos), Point::new(x_pos - 2, b_y_pos)) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::CSS_DARK_GREEN, 1)) + .draw(&mut framebuf) + .unwrap(); + + x_pos -= 2; + } + } // Draw the text to the screen Text::with_alignment( @@ -105,7 +180,10 @@ pub async fn display_output_task( .unwrap(); // Draw the entire framebuffer to the display - let area: Rectangle = Rectangle::new(Point::new(0, 0), Size::new(128, 128)); + let area: Rectangle = Rectangle::new( + Point::new(0, 0), + Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32), + ); display.fill_contiguous(&area, framebuf.data).unwrap(); } }