diff --git a/Cargo.lock b/Cargo.lock index aae1f1a..3cb1cbb 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" @@ -424,6 +430,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "embedded-graphics" version = "0.8.1" @@ -447,6 +462,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "embedded-graphics-framebuf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22354420f68727fa24d1e2741dae1e9a041065e80fb63b35a8d19c647a85be76" +dependencies = [ + "embedded-dma", + "embedded-graphics", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -678,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" @@ -689,7 +723,7 @@ dependencies = [ "diff", "ena", "is-terminal", - "itertools", + "itertools 0.10.5", "lalrpop-util", "petgraph", "regex", @@ -861,6 +895,7 @@ dependencies = [ name = "pico-enviro-sensor" version = "0.1.0" dependencies = [ + "circular-buffer", "cortex-m-rt", "display-interface-spi", "embassy-embedded-hal", @@ -869,7 +904,10 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-graphics", + "embedded-graphics-framebuf", "heapless", + "itertools 0.14.0", + "num-traits", "rtt-target", "scd4x", "ssd1351", diff --git a/Cargo.toml b/Cargo.toml index 695e6ad..fae92d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,12 +25,15 @@ embassy-time = "0.4.0" embassy-sync = "0.6.2" embassy-embedded-hal = "0.3.0" static_cell = "2.1.0" -embedded-graphics = "0.8.1" # System cortex-m-rt = "0.7.5" rtt-target = "0.6.1" +# embedded-graphics +embedded-graphics = "0.8.1" +embedded-graphics-framebuf = "0.5.0" + # Peripherals scd4x = { git = "https://github.com/twokilohertz/scd4x-rs.git", branch = "conversion-fixes", features = [ "scd41", @@ -40,6 +43,9 @@ 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 } +num-traits = { version = "0.2.19", default-features = false } [profile.dev] opt-level = "s" diff --git a/src/display_task.rs b/src/display_task.rs index 1dcdba0..be88c7a 100644 --- a/src/display_task.rs +++ b/src/display_task.rs @@ -1,6 +1,7 @@ // System use embassy_embedded_hal::shared_bus::blocking::spi::SpiDeviceWithConfig; use embassy_rp::{gpio::Output, spi::Config}; +use embedded_graphics_framebuf::FrameBuf; use rtt_target::rprintln; // Display @@ -13,15 +14,27 @@ use ssd1351::{ // Graphics use embedded_graphics::{ + draw_target::DrawTarget, mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::Rgb565, - prelude::{Point, WebColors}, - text::{Alignment, Text}, + prelude::{Point, Primitive, RgbColor, Size, WebColors}, + primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, + text::{Alignment, Text, TextStyleBuilder}, Drawable, }; -use crate::{Spi0BusMutex, SENSOR_DATA_SIGNAL}; +// Containers +use circular_buffer::CircularBuffer; +use heapless::String; +use itertools::Itertools; + use core::fmt::Write; +use num_traits::{Num, NumCast}; + +use crate::{Spi0BusMutex, SENSOR_DATA_SIGNAL}; + +const DISPLAY_WIDTH: usize = 128; +const DISPLAY_HEIGHT: usize = 128; /// Output to the SSD1351 display #[embassy_executor::task] @@ -46,55 +59,209 @@ pub async fn display_output_task( display.reset(rst, &mut embassy_time::Delay).unwrap(); display.init().unwrap(); - 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); + // Framebuffer, prevents flickering on redraw + let buf = [Rgb565::BLACK; DISPLAY_WIDTH * DISPLAY_HEIGHT]; + let mut framebuf = FrameBuf::new(buf, DISPLAY_WIDTH, DISPLAY_HEIGHT); - 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(); + // Text styles + let align_l_base_bottom = TextStyleBuilder::new() + .alignment(Alignment::Left) + .baseline(embedded_graphics::text::Baseline::Bottom) + .build(); + let co2_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_FOREST_GREEN); + let temp_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_ORANGE_RED); + let humidity_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_AQUAMARINE); + + // 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 contents of buffers + // Clear the framebuffer + framebuf.clear(Rgb565::BLACK).unwrap(); + + // Clear contents of text buffers co2_text_buf.clear(); temp_text_buf.clear(); 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(); - // Clear the display - display.clear(); + // 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 { + draw_line_graph( + &co2_samples, + co2_max.into(), + co2_min.into(), + 4, + Rgb565::CSS_DARK_GREEN, + Some(Rgb565::new(3, 5, 3)), + &mut framebuf, + ) + .unwrap(); + } + + if temp_samples.len() >= 2 { + draw_line_graph( + &temp_samples, + temp_max as i32, + temp_min as i32, + 38, + Rgb565::CSS_ORANGE, + Some(Rgb565::new(3, 5, 3)), + &mut framebuf, + ) + .unwrap(); + } + + if humidity_samples.len() >= 2 { + draw_line_graph( + &humidity_samples, + humid_max as i32, + humid_min as i32, + 72, + Rgb565::CSS_AQUA, + Some(Rgb565::new(3, 5, 3)), + &mut framebuf, + ) + .unwrap(); + } // Draw the text to the screen - Text::with_alignment( + + Text::with_text_style( &co2_text_buf, - Point::new(1, 12), + Point::new(5, 34), co2_text_style, - Alignment::Left, + align_l_base_bottom, ) - .draw(&mut display) + .draw(&mut framebuf) .unwrap(); - Text::with_alignment( + Text::with_text_style( &temp_text_buf, - Point::new(1, 24), + Point::new(5, 68), temp_text_style, - Alignment::Left, + align_l_base_bottom, ) - .draw(&mut display) + .draw(&mut framebuf) .unwrap(); - Text::with_alignment( + Text::with_text_style( &humidity_text_buf, - Point::new(1, 36), + Point::new(5, 102), humidity_text_style, - Alignment::Left, + align_l_base_bottom, ) - .draw(&mut display) + .draw(&mut framebuf) .unwrap(); + + // Draw the entire framebuffer to the display + + 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(); + } + + fn draw_line_graph<'a, I, V, D, C>( + collection: I, + graph_max: i32, + graph_min: i32, + y_start: i32, + line_colour: C, + back_colour: Option, + target: &mut D, + ) -> Result<(), D::Error> + where + I: IntoIterator, + I::IntoIter: DoubleEndedIterator, + V: ?Sized + 'a + Num + NumCast + Clone, + C: RgbColor, + D: DrawTarget, + { + let mut x_pos: i32 = (DISPLAY_WIDTH - 5) as i32; + + match back_colour { + Some(c) => { + let style = PrimitiveStyleBuilder::new().fill_color(c).build(); + + Rectangle::new(Point::new(4, y_start), Size::new(120, 30)) + .into_styled(style) + .draw(target)?; + } + None => {} + } + + for (a, b) in collection.into_iter().rev().tuple_windows::<(_, _)>() { + let range: i32 = if (graph_max - graph_min) == 0 { + 1_i32 + } else { + graph_max - graph_min + }; + + let a_i32: i32 = match NumCast::from(a.clone()) { + Some(v) => v, + None => 0, + }; + let b_i32: i32 = match NumCast::from(b.clone()) { + Some(v) => v, + None => 0, + }; + + let a_y_pos: i32 = y_start + (((graph_max - a_i32) * 30) / range); + let b_y_pos: i32 = y_start + (((graph_max - b_i32) * 30) / range); + + Line::new(Point::new(x_pos, a_y_pos), Point::new(x_pos - 2, b_y_pos)) + .into_styled(PrimitiveStyle::with_stroke(line_colour, 1)) + .draw(target)?; + + x_pos -= 2; + } + + return Ok(()); } }