From aee648bfa5e94e12a9dd8478fc45c4e8db0bb1f9 Mon Sep 17 00:00:00 2001 From: Adam Macdonald <72780006+twokilohertz@users.noreply.github.com> Date: Wed, 5 Mar 2025 18:44:12 +0000 Subject: [PATCH 1/3] Add framebuffer to prevent redraw flicker --- Cargo.lock | 20 ++++++++++++++++++++ Cargo.toml | 5 ++++- src/display_task.rs | 27 +++++++++++++++++++-------- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aae1f1a..bc07f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,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 +456,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" @@ -869,6 +888,7 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-graphics", + "embedded-graphics-framebuf", "heapless", "rtt-target", "scd4x", diff --git a/Cargo.toml b/Cargo.toml index 695e6ad..dd6fd6f 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", diff --git a/src/display_task.rs b/src/display_task.rs index 1dcdba0..440bb9a 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,9 +14,11 @@ use ssd1351::{ // Graphics use embedded_graphics::{ + draw_target::DrawTarget, mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::Rgb565, - prelude::{Point, WebColors}, + prelude::{Point, RgbColor, Size, WebColors}, + primitives::Rectangle, text::{Alignment, Text}, Drawable, }; @@ -46,6 +49,10 @@ pub async fn display_output_task( display.reset(rst, &mut embassy_time::Delay).unwrap(); display.init().unwrap(); + // Framebuffer, prevents flickering on redraw + let buf = [Rgb565::BLACK; 128 * 128]; + let mut framebuf = FrameBuf::new(buf, 128, 128); + 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); @@ -55,7 +62,10 @@ pub async fn display_output_task( let mut humidity_text_buf = heapless::String::<16>::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(); @@ -66,9 +76,6 @@ pub async fn display_output_task( write!(&mut temp_text_buf, "Temp: {:.1} C", data.temperature).unwrap(); write!(&mut humidity_text_buf, "RH: {:.1} %", data.humidity).unwrap(); - // Clear the display - display.clear(); - // Draw the text to the screen Text::with_alignment( &co2_text_buf, @@ -76,7 +83,7 @@ pub async fn display_output_task( co2_text_style, Alignment::Left, ) - .draw(&mut display) + .draw(&mut framebuf) .unwrap(); Text::with_alignment( @@ -85,7 +92,7 @@ pub async fn display_output_task( temp_text_style, Alignment::Left, ) - .draw(&mut display) + .draw(&mut framebuf) .unwrap(); Text::with_alignment( @@ -94,7 +101,11 @@ pub async fn display_output_task( humidity_text_style, Alignment::Left, ) - .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(128, 128)); + display.fill_contiguous(&area, framebuf.data).unwrap(); } } 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 2/3] 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(); } } From 6698b1ed69ac91012494cc3841c62ff93885d987 Mon Sep 17 00:00:00 2001 From: Adam Macdonald <72780006+twokilohertz@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:12:44 +0000 Subject: [PATCH 3/3] Drawing line graphs for all metrics --- Cargo.lock | 1 + Cargo.toml | 1 + src/display_task.rs | 160 ++++++++++++++++++++++++++++++++------------ 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71b0c1f..3cb1cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,7 @@ dependencies = [ "embedded-graphics-framebuf", "heapless", "itertools 0.14.0", + "num-traits", "rtt-target", "scd4x", "ssd1351", diff --git a/Cargo.toml b/Cargo.toml index 56f6936..fae92d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ display-interface-spi = "0.5.0" 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 448752b..be88c7a 100644 --- a/src/display_task.rs +++ b/src/display_task.rs @@ -18,8 +18,8 @@ use embedded_graphics::{ mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::Rgb565, prelude::{Point, Primitive, RgbColor, Size, WebColors}, - primitives::{Line, PrimitiveStyle, Rectangle}, - text::{Alignment, Text}, + primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, + text::{Alignment, Text, TextStyleBuilder}, Drawable, }; @@ -28,11 +28,10 @@ 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}; -use core::{ - fmt::Write, - ops::{Add, Div, Sub}, -}; const DISPLAY_WIDTH: usize = 128; const DISPLAY_HEIGHT: usize = 128; @@ -65,9 +64,13 @@ pub async fn display_output_task( 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 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(); @@ -101,19 +104,19 @@ pub async fn display_output_task( let co2_min = *co2_samples.iter().min().unwrap(); let co2_max = *co2_samples.iter().max().unwrap(); - let _temp_min = *temp_samples + let temp_min = *temp_samples .iter() .reduce(|a: &f32, b: &f32| if a.le(b) { a } else { b }) .unwrap(); - let _temp_max = *temp_samples + let temp_max = *temp_samples .iter() .reduce(|a: &f32, b: &f32| if a.ge(b) { a } else { b }) .unwrap(); - let _humid_min = *humidity_samples + let humid_min = *humidity_samples .iter() .reduce(|a: &f32, b: &f32| if a.le(b) { a } else { b }) .unwrap(); - let _humid_max = *humidity_samples + let humid_max = *humidity_samples .iter() .reduce(|a: &f32, b: &f32| if a.ge(b) { a } else { b }) .unwrap(); @@ -128,62 +131,137 @@ pub async fn display_output_task( // Draw line graphs if co2_samples.len() >= 2 { - // CO2 line graph happens 4 pixels from the top, with 4 pixels either side + 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(); + } - let mut x_pos: i32 = (DISPLAY_WIDTH - 4) as i32; + 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(); + } - 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; - } + 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 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 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 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(()); + } }