From 325597696100a8814125bbe50dc8bdf2c9f4dc40 Mon Sep 17 00:00:00 2001 From: Adam Macdonald Date: Sun, 8 Jun 2025 01:16:38 +0100 Subject: [PATCH] Improve line graph drawing function & begin input handling --- .cargo/config.toml | 14 +-- Cargo.lock | 20 ++-- Cargo.toml | 10 +- scripts/env-vars.sh | 2 +- src/display_task.rs | 254 ++++++++++++++++++++++---------------------- src/input_task.rs | 60 +++++++++++ src/main.rs | 21 ++-- src/sensor_task.rs | 14 +-- 8 files changed, 224 insertions(+), 171 deletions(-) create mode 100644 src/input_task.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index b5945e5..927ef0a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,11 +1,3 @@ -[alias] - -build-arm = "build --target=thumbv8m.main-none-eabihf" -build-riscv = "build --target=riscv32imac-unknown-none-elf" - -run-arm = "run --target=thumbv8m.main-none-eabihf" -run-riscv = "run --target=riscv32imac-unknown-none-elf" - [build] target = "thumbv8m.main-none-eabihf" @@ -18,10 +10,8 @@ rustflags = [ "-C", "target-cpu=cortex-m33", ] - -runner = "picotool load -u -v -x -t elf" +runner = "probe-rs run --chip RP235x" [target.riscv32imac-unknown-none-elf] rustflags = ["-C", "link-arg=--nmagic", "-C", "link-arg=-Trp235x_riscv.x"] - -runner = "picotool load -u -v -x -t elf" +runner = "probe-rs run --chip RP235x" diff --git a/Cargo.lock b/Cargo.lock index 3cb1cbb..9f50cdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,15 +703,6 @@ 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" @@ -723,7 +714,7 @@ dependencies = [ "diff", "ena", "is-terminal", - "itertools 0.10.5", + "itertools", "lalrpop-util", "petgraph", "regex", @@ -896,18 +887,20 @@ name = "pico-enviro-sensor" version = "0.1.0" dependencies = [ "circular-buffer", + "cortex-m", "cortex-m-rt", + "critical-section", "display-interface-spi", "embassy-embedded-hal", "embassy-executor", + "embassy-futures", "embassy-rp", "embassy-sync", "embassy-time", "embedded-graphics", "embedded-graphics-framebuf", + "fixed", "heapless", - "itertools 0.14.0", - "num-traits", "rtt-target", "scd4x", "ssd1351", @@ -1144,7 +1137,8 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "scd4x" version = "0.4.0" -source = "git+https://github.com/twokilohertz/scd4x-rs.git?branch=conversion-fixes#07730523c51d6909530c000de271832125760385" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550fedc97e7880654aa7dc840f5eb3b2201a01aae566c4584a3601bb0086cd4c" dependencies = [ "embedded-hal 1.0.0", "log", diff --git a/Cargo.toml b/Cargo.toml index 247902d..4dc80bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,14 @@ embassy-executor = { version = "0.7.0", features = [ "task-arena-size-65536", ] } embassy-time = "0.4.0" -embassy-sync = "0.6.2" embassy-embedded-hal = "0.3.0" -static_cell = "2.1.0" +embassy-futures = "0.1.1" +embassy-sync = "0.6.2" # System cortex-m-rt = "0.7.5" +cortex-m = "0.7.7" +critical-section = "1.2.0" rtt-target = "0.6.1" # embedded-graphics @@ -40,10 +42,10 @@ ssd1351 = "0.5.0" display-interface-spi = "0.5.0" # Extra +static_cell = "2.1.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 } +fixed = "1.29.0" [profile.dev] opt-level = "s" diff --git a/scripts/env-vars.sh b/scripts/env-vars.sh index 5ec95df..ddd083a 100755 --- a/scripts/env-vars.sh +++ b/scripts/env-vars.sh @@ -1,3 +1,3 @@ export PICO_BOARD=pico2_w export PICO_PLATFORM=rp2350-arm-s -export PICO_SDK_PATH=~/devel/clones/pico-sdk/ +export PICO_SDK_PATH=~/devel/clones/pico-sdk diff --git a/src/display_task.rs b/src/display_task.rs index be88c7a..4a8e86c 100644 --- a/src/display_task.rs +++ b/src/display_task.rs @@ -1,8 +1,11 @@ +use core::fmt::Write; + // 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; +use fixed::types::U16F16; +use rtt_target::debug_rprintln; // Display use display_interface_spi::SPIInterface; @@ -16,9 +19,9 @@ use ssd1351::{ use embedded_graphics::{ draw_target::DrawTarget, mono_font::{ascii::FONT_6X10, MonoTextStyle}, - pixelcolor::Rgb565, + pixelcolor::{Rgb565, Rgb888}, prelude::{Point, Primitive, RgbColor, Size, WebColors}, - primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, + primitives::{Line, PrimitiveStyle, Rectangle, StyledDrawable}, text::{Alignment, Text, TextStyleBuilder}, Drawable, }; @@ -26,15 +29,13 @@ use embedded_graphics::{ // 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; +const DISPLAY_PADDING: usize = 5; +type SensorDataBuffer = CircularBuffer<60, U16F16>; /// Output to the SSD1351 display #[embassy_executor::task] @@ -45,7 +46,7 @@ pub async fn display_output_task( rst: &'static mut Output<'static>, spi_config: Config, ) { - rprintln!("Display output task started"); + debug_rprintln!("Display output task started"); let spi_dev = SpiDeviceWithConfig::new(spi_bus, cs, spi_config); let interface = SPIInterface::new(spi_dev, dc); @@ -73,14 +74,14 @@ pub async fn display_output_task( 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(); + let mut co2_text_buf = String::<20>::new(); + let mut temp_text_buf = String::<20>::new(); + let mut humidity_text_buf = String::<20>::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(); + let mut co2_samples = SensorDataBuffer::new(); + let mut temp_samples = SensorDataBuffer::new(); + let mut humidity_samples = SensorDataBuffer::new(); loop { // Clear the framebuffer @@ -98,28 +99,19 @@ pub async fn display_output_task( 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); + co2_samples.push_back(U16F16::from_num(sensor_data.co2)); + temp_samples.push_back(U16F16::from_num(sensor_data.temperature)); + humidity_samples.push_back(U16F16::from_num(sensor_data.humidity)); + co2_samples.make_contiguous(); + temp_samples.make_contiguous(); + humidity_samples.make_contiguous(); 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(); + let temp_min = *temp_samples.iter().min().unwrap(); + let temp_max = *temp_samples.iter().max().unwrap(); + let humid_min = *humidity_samples.iter().min().unwrap(); + let humid_max = *humidity_samples.iter().max().unwrap(); /* Note about drawing positions: @@ -130,44 +122,53 @@ pub async fn display_output_task( // 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(); - } + const LINE_GRAPH_WIDTH: u32 = (DISPLAY_WIDTH - (DISPLAY_PADDING * 2)) as u32; + const LINE_GRAPH_HEIGHT: u32 = 36; - 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(); - } + draw_line_graph( + Rectangle::new( + Point::new(DISPLAY_PADDING as i32, DISPLAY_PADDING as i32), + Size::new(LINE_GRAPH_WIDTH, LINE_GRAPH_HEIGHT), + ), + co2_min, + co2_max, + co2_samples.as_slices().0, + Rgb565::CSS_DARK_GREEN, + Some(Rgb888::new(24, 24, 24).into()), + &mut framebuf, + ); - 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_line_graph( + Rectangle::new( + Point::new( + DISPLAY_PADDING as i32, + (LINE_GRAPH_HEIGHT + (DISPLAY_PADDING as u32 * 2)) as i32, + ), + Size::new(LINE_GRAPH_WIDTH, LINE_GRAPH_HEIGHT), + ), + temp_min, + temp_max, + temp_samples.as_slices().0, + Rgb565::CSS_ORANGE, + Some(Rgb888::new(24, 24, 24).into()), + &mut framebuf, + ); + + draw_line_graph( + Rectangle::new( + Point::new( + DISPLAY_PADDING as i32, + ((LINE_GRAPH_HEIGHT * 2) + (DISPLAY_PADDING as u32 * 3)) as i32, + ), + Size::new(LINE_GRAPH_WIDTH, LINE_GRAPH_HEIGHT), + ), + humid_min, + humid_max, + humidity_samples.as_slices().0, + Rgb565::CSS_AQUA, + Some(Rgb888::new(24, 24, 24).into()), + &mut framebuf, + ); // Draw the text to the screen @@ -206,62 +207,61 @@ pub async fn display_output_task( ); 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(()); - } +} + +fn draw_line_graph( + bounds: Rectangle, + y_min: U16F16, + y_max: U16F16, + samples: &[U16F16], + fg_colour: C, + bg_colour: Option, + target: &mut D, +) where + C: RgbColor, + D: DrawTarget, +{ + // Draw background colour first, if supplied + + if let Some(bg_col) = bg_colour { + let _ = bounds.draw_styled(&PrimitiveStyle::with_fill(bg_col), target); + } + + // Draw the data points + + if samples.len() < 2 { + // Drawing a line requires a minimum of two points + return; + } + + let graph_width = U16F16::from_num(bounds.size.width); + let graph_height = U16F16::from_num(bounds.size.height); + let y_range = y_max - y_min; + let mut x_offset: i32 = bounds.top_left.x; + let n_samples: U16F16 = U16F16::from_num(samples.len()); + let mut n_samples_seen: U16F16 = U16F16::from_num(1); + + for sample in samples.windows(2) { + let x_pos_start: i32 = x_offset; + let y_pos_start: i32 = bounds.top_left.y + + (graph_height - (((sample[0] - y_min) / y_range) * graph_height)).to_num::(); + let x_pos_end: i32 = bounds.top_left.x + + (((n_samples_seen + U16F16::from_num(1)) / n_samples) + * (graph_width - U16F16::from_num(1))) + .to_num::(); + let y_pos_end: i32 = bounds.top_left.y + + (graph_height - (((sample[1] - y_min) / y_range) * graph_height)).to_num::(); + + let _ = Line::new( + Point::new(x_pos_start, y_pos_start), + Point::new(x_pos_end, y_pos_end), + ) + .into_styled(PrimitiveStyle::with_stroke(fg_colour, 1)) + .draw(target); + + x_offset = x_pos_end; + n_samples_seen += U16F16::from_num(1); + } + + return; } diff --git a/src/input_task.rs b/src/input_task.rs new file mode 100644 index 0000000..de6c981 --- /dev/null +++ b/src/input_task.rs @@ -0,0 +1,60 @@ +use embassy_futures::select::{select3, Either3}; +use embassy_rp::gpio::Input; +use embassy_time::{with_timeout, Duration, Instant}; +use rtt_target::debug_rprintln; + +const DEBOUNCE_TIME_MILLIS: u64 = 20; + +#[embassy_executor::task] +pub async fn input_handling_task( + mut button_enter: Input<'static>, + mut button_left: Input<'static>, + mut button_right: Input<'static>, +) { + debug_rprintln!("Input handling task started"); + + loop { + let enter_pressed = button_enter.wait_for_falling_edge(); + let left_pressed = button_left.wait_for_falling_edge(); + let right_pressed = button_right.wait_for_falling_edge(); + + match select3(enter_pressed, left_pressed, right_pressed).await { + Either3::First(_) => { + let pressed_at = Instant::now(); + if with_timeout(Duration::from_secs(2), button_enter.wait_for_rising_edge()) + .await + .is_ok() + { + if pressed_at.elapsed().as_millis() < DEBOUNCE_TIME_MILLIS { + continue; + } + debug_rprintln!("Enter button pressed!"); + } + } + Either3::Second(_) => { + let pressed_at = Instant::now(); + if with_timeout(Duration::from_secs(2), button_left.wait_for_rising_edge()) + .await + .is_ok() + { + if pressed_at.elapsed().as_millis() < DEBOUNCE_TIME_MILLIS { + continue; + } + debug_rprintln!("Left button pressed!"); + } + } + Either3::Third(_) => { + let pressed_at = Instant::now(); + if with_timeout(Duration::from_secs(2), button_right.wait_for_rising_edge()) + .await + .is_ok() + { + if pressed_at.elapsed().as_millis() < DEBOUNCE_TIME_MILLIS { + continue; + } + debug_rprintln!("Right button pressed!"); + } + } + }; + } +} diff --git a/src/main.rs b/src/main.rs index 1f7eb9a..3ed5d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,16 +2,17 @@ #![no_main] mod display_task; +mod input_task; mod sensor_task; use core::cell::RefCell; -use rtt_target::{rprintln, rtt_init_print}; +use rtt_target::{debug_rprintln, debug_rtt_init_print}; use embassy_executor::Spawner; use embassy_rp::{ block::ImageDef, - gpio::{Level, Output}, + gpio::{Input, Level, Output, Pull}, i2c, peripherals::{I2C0, SPI0}, spi, @@ -27,6 +28,7 @@ use embassy_time::Timer; use static_cell::StaticCell; use display_task::display_output_task; +use input_task::input_handling_task; use sensor_task::sensor_read_task; type I2c0BusMutex = Mutex>>; @@ -41,11 +43,11 @@ static SENSOR_DATA_SIGNAL: Signal ! { // Initialise RTT logging - rtt_init_print!(); - rprintln!("RTT logging initialised"); + debug_rtt_init_print!(); + debug_rprintln!("RTT logging initialised"); let peripherals = embassy_rp::init(Default::default()); @@ -70,7 +72,7 @@ async fn main(spawner: Spawner) { let rst = SPI0_RST_PIN.init(Output::new(peripherals.PIN_15, Level::Low)); let mut spi_config = spi::Config::default(); - spi_config.frequency = 4_000_000u32; // 4 MHz + spi_config.frequency = 20_000_000u32; // 20 MHz let spi_bus = spi::Spi::new_blocking_txonly(peripherals.SPI0, sclk, mosi, spi_config.clone()); static SPI0_BUS: StaticCell = StaticCell::new(); let shared_spi0_bus = SPI0_BUS.init(Mutex::new(RefCell::new(spi_bus))); @@ -84,6 +86,11 @@ async fn main(spawner: Spawner) { spi_config.clone(), )); + let enter_button = Input::new(peripherals.PIN_8, Pull::Up); + let left_button = Input::new(peripherals.PIN_6, Pull::Up); + let right_button = Input::new(peripherals.PIN_7, Pull::Up); + spawner.must_spawn(input_handling_task(enter_button, left_button, right_button)); + loop { Timer::after_secs(1).await; } @@ -92,7 +99,7 @@ async fn main(spawner: Spawner) { /// Panic handler #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { - rprintln!("Panicked! {}", info); + debug_rprintln!("Panicked! {}", info); loop { unsafe { diff --git a/src/sensor_task.rs b/src/sensor_task.rs index a12065d..97f960e 100644 --- a/src/sensor_task.rs +++ b/src/sensor_task.rs @@ -1,7 +1,7 @@ // System use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice; use embassy_time::Timer; -use rtt_target::rprintln; +use rtt_target::debug_rprintln; // Sensor use scd4x::Scd4x; @@ -11,7 +11,7 @@ use crate::{I2c0BusMutex, SENSOR_DATA_SIGNAL}; /// Read CO2/temp./humidity data from the sensor #[embassy_executor::task] pub async fn sensor_read_task(i2c_bus: &'static I2c0BusMutex) { - rprintln!("Sensor read task started"); + debug_rprintln!("Sensor read task started"); Timer::after_millis(30).await; // SCD41 power-up delay let i2c_dev = I2cDevice::new(i2c_bus); @@ -21,8 +21,8 @@ pub async fn sensor_read_task(i2c_bus: &'static I2c0BusMutex) { scd41.reinit().unwrap(); match scd41.serial_number() { - Ok(serial) => rprintln!("[SCD41] Serial number: {}", serial), - Err(error) => rprintln!( + Ok(serial) => debug_rprintln!("[SCD41] Serial number: {}", serial), + Err(error) => debug_rprintln!( "[SCD41] Error: did not respond to get_serial_number: {:?}", error ), @@ -35,8 +35,8 @@ pub async fn sensor_read_task(i2c_bus: &'static I2c0BusMutex) { match scd41.measurement() { Ok(data) => { - rprintln!( - "[SCD41] CO2: {} ppm, temperature: {} C, humidity: {} RH", + debug_rprintln!( + "[SCD41] CO2: {} ppm, temperature: {} C, humidity: {} % RH", data.co2, data.temperature, data.humidity @@ -45,7 +45,7 @@ pub async fn sensor_read_task(i2c_bus: &'static I2c0BusMutex) { SENSOR_DATA_SIGNAL.signal(data); } Err(error) => { - rprintln!( + debug_rprintln!( "[SCD41] Error: failed to retrieve measurement data: {:?}", error );