pico-enviro-sensor/src/display_task.rs
2025-03-05 21:45:06 +00:00

190 lines
6.0 KiB
Rust

// 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
use display_interface_spi::SPIInterface;
use ssd1351::{
builder::Builder,
mode::GraphicsMode,
properties::{DisplayRotation, DisplaySize},
};
// Graphics
use embedded_graphics::{
draw_target::DrawTarget,
mono_font::{ascii::FONT_6X10, MonoTextStyle},
pixelcolor::Rgb565,
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,
ops::{Add, Div, Sub},
};
const DISPLAY_WIDTH: usize = 128;
const DISPLAY_HEIGHT: usize = 128;
/// Output to the SSD1351 display
#[embassy_executor::task]
pub async fn display_output_task(
spi_bus: &'static Spi0BusMutex,
cs: Output<'static>,
dc: Output<'static>,
rst: &'static mut Output<'static>,
spi_config: Config,
) {
rprintln!("Display output task started");
let spi_dev = SpiDeviceWithConfig::new(spi_bus, cs, spi_config);
let interface = SPIInterface::new(spi_dev, dc);
let mut display: GraphicsMode<_> = Builder::new()
.with_size(DisplaySize::Display128x128)
.with_rotation(DisplayRotation::Rotate0)
.connect_interface(interface)
.into();
display.reset(rst, &mut embassy_time::Delay).unwrap();
display.init().unwrap();
// Framebuffer, prevents flickering on redraw
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);
// 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
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 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(
&co2_text_buf,
Point::new(1, 12),
co2_text_style,
Alignment::Left,
)
.draw(&mut framebuf)
.unwrap();
Text::with_alignment(
&temp_text_buf,
Point::new(1, 24),
temp_text_style,
Alignment::Left,
)
.draw(&mut framebuf)
.unwrap();
Text::with_alignment(
&humidity_text_buf,
Point::new(1, 36),
humidity_text_style,
Alignment::Left,
)
.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();
}
}