Merge pull request #1 from twokilohertz/line-graphs

Line graphs
This commit is contained in:
Adam Macdonald 2025-03-06 17:13:15 +00:00 committed by GitHub
commit d55e93603e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 241 additions and 30 deletions

40
Cargo.lock generated
View File

@ -107,6 +107,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "circular-buffer"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f"
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -424,6 +430,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" 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]] [[package]]
name = "embedded-graphics" name = "embedded-graphics"
version = "0.8.1" version = "0.8.1"
@ -447,6 +462,16 @@ dependencies = [
"byteorder", "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]] [[package]]
name = "embedded-hal" name = "embedded-hal"
version = "0.2.7" version = "0.2.7"
@ -678,6 +703,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "lalrpop" name = "lalrpop"
version = "0.19.12" version = "0.19.12"
@ -689,7 +723,7 @@ dependencies = [
"diff", "diff",
"ena", "ena",
"is-terminal", "is-terminal",
"itertools", "itertools 0.10.5",
"lalrpop-util", "lalrpop-util",
"petgraph", "petgraph",
"regex", "regex",
@ -861,6 +895,7 @@ dependencies = [
name = "pico-enviro-sensor" name = "pico-enviro-sensor"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"circular-buffer",
"cortex-m-rt", "cortex-m-rt",
"display-interface-spi", "display-interface-spi",
"embassy-embedded-hal", "embassy-embedded-hal",
@ -869,7 +904,10 @@ dependencies = [
"embassy-sync", "embassy-sync",
"embassy-time", "embassy-time",
"embedded-graphics", "embedded-graphics",
"embedded-graphics-framebuf",
"heapless", "heapless",
"itertools 0.14.0",
"num-traits",
"rtt-target", "rtt-target",
"scd4x", "scd4x",
"ssd1351", "ssd1351",

View File

@ -25,12 +25,15 @@ embassy-time = "0.4.0"
embassy-sync = "0.6.2" embassy-sync = "0.6.2"
embassy-embedded-hal = "0.3.0" embassy-embedded-hal = "0.3.0"
static_cell = "2.1.0" static_cell = "2.1.0"
embedded-graphics = "0.8.1"
# System # System
cortex-m-rt = "0.7.5" cortex-m-rt = "0.7.5"
rtt-target = "0.6.1" rtt-target = "0.6.1"
# embedded-graphics
embedded-graphics = "0.8.1"
embedded-graphics-framebuf = "0.5.0"
# Peripherals # Peripherals
scd4x = { git = "https://github.com/twokilohertz/scd4x-rs.git", branch = "conversion-fixes", features = [ scd4x = { git = "https://github.com/twokilohertz/scd4x-rs.git", branch = "conversion-fixes", features = [
"scd41", "scd41",
@ -40,6 +43,9 @@ display-interface-spi = "0.5.0"
# Extra # Extra
heapless = "0.8.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] [profile.dev]
opt-level = "s" opt-level = "s"

View File

@ -1,6 +1,7 @@
// System // System
use embassy_embedded_hal::shared_bus::blocking::spi::SpiDeviceWithConfig; use embassy_embedded_hal::shared_bus::blocking::spi::SpiDeviceWithConfig;
use embassy_rp::{gpio::Output, spi::Config}; use embassy_rp::{gpio::Output, spi::Config};
use embedded_graphics_framebuf::FrameBuf;
use rtt_target::rprintln; use rtt_target::rprintln;
// Display // Display
@ -13,15 +14,27 @@ use ssd1351::{
// Graphics // Graphics
use embedded_graphics::{ use embedded_graphics::{
draw_target::DrawTarget,
mono_font::{ascii::FONT_6X10, MonoTextStyle}, mono_font::{ascii::FONT_6X10, MonoTextStyle},
pixelcolor::Rgb565, pixelcolor::Rgb565,
prelude::{Point, WebColors}, prelude::{Point, Primitive, RgbColor, Size, WebColors},
text::{Alignment, Text}, primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
text::{Alignment, Text, TextStyleBuilder},
Drawable, Drawable,
}; };
use crate::{Spi0BusMutex, SENSOR_DATA_SIGNAL}; // Containers
use circular_buffer::CircularBuffer;
use heapless::String;
use itertools::Itertools;
use core::fmt::Write; 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 /// Output to the SSD1351 display
#[embassy_executor::task] #[embassy_executor::task]
@ -46,55 +59,209 @@ pub async fn display_output_task(
display.reset(rst, &mut embassy_time::Delay).unwrap(); display.reset(rst, &mut embassy_time::Delay).unwrap();
display.init().unwrap(); display.init().unwrap();
let co2_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_DARK_GREEN); // Framebuffer, prevents flickering on redraw
let temp_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_ORANGE); let buf = [Rgb565::BLACK; DISPLAY_WIDTH * DISPLAY_HEIGHT];
let humidity_text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_AQUA); let mut framebuf = FrameBuf::new(buf, DISPLAY_WIDTH, DISPLAY_HEIGHT);
let mut co2_text_buf = heapless::String::<16>::new(); // Text styles
let mut temp_text_buf = heapless::String::<16>::new(); let align_l_base_bottom = TextStyleBuilder::new()
let mut humidity_text_buf = heapless::String::<16>::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 { loop {
// Clear contents of buffers // Clear the framebuffer
framebuf.clear(Rgb565::BLACK).unwrap();
// Clear contents of text buffers
co2_text_buf.clear(); co2_text_buf.clear();
temp_text_buf.clear(); temp_text_buf.clear();
humidity_text_buf.clear(); humidity_text_buf.clear();
// Wait on sensor data & format into the buffers // Wait on sensor data & format into the buffers
let data = SENSOR_DATA_SIGNAL.wait().await; let sensor_data = SENSOR_DATA_SIGNAL.wait().await;
write!(&mut co2_text_buf, "CO2: {} ppm", data.co2).unwrap(); write!(&mut co2_text_buf, "CO2: {} ppm", sensor_data.co2).unwrap();
write!(&mut temp_text_buf, "Temp: {:.1} C", data.temperature).unwrap(); write!(&mut temp_text_buf, "Temp: {:.1} C", sensor_data.temperature).unwrap();
write!(&mut humidity_text_buf, "RH: {:.1} %", data.humidity).unwrap(); write!(&mut humidity_text_buf, "RH: {:.1} %", sensor_data.humidity).unwrap();
// Clear the display // Record samples
display.clear(); 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 // Draw the text to the screen
Text::with_alignment(
Text::with_text_style(
&co2_text_buf, &co2_text_buf,
Point::new(1, 12), Point::new(5, 34),
co2_text_style, co2_text_style,
Alignment::Left, align_l_base_bottom,
) )
.draw(&mut display) .draw(&mut framebuf)
.unwrap(); .unwrap();
Text::with_alignment( Text::with_text_style(
&temp_text_buf, &temp_text_buf,
Point::new(1, 24), Point::new(5, 68),
temp_text_style, temp_text_style,
Alignment::Left, align_l_base_bottom,
) )
.draw(&mut display) .draw(&mut framebuf)
.unwrap(); .unwrap();
Text::with_alignment( Text::with_text_style(
&humidity_text_buf, &humidity_text_buf,
Point::new(1, 36), Point::new(5, 102),
humidity_text_style, humidity_text_style,
Alignment::Left, align_l_base_bottom,
) )
.draw(&mut display) .draw(&mut framebuf)
.unwrap(); .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<C>,
target: &mut D,
) -> Result<(), D::Error>
where
I: IntoIterator<Item = &'a V>,
I::IntoIter: DoubleEndedIterator,
V: ?Sized + 'a + Num + NumCast + Clone,
C: RgbColor,
D: DrawTarget<Color = C>,
{
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(());
} }
} }