Improve line graph drawing function & begin input handling

This commit is contained in:
Adam 2025-06-08 01:16:38 +01:00
parent 219e43fee8
commit 3255976961
8 changed files with 224 additions and 171 deletions

View File

@ -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"

20
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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<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(());
}
}
fn draw_line_graph<C, D>(
bounds: Rectangle,
y_min: U16F16,
y_max: U16F16,
samples: &[U16F16],
fg_colour: C,
bg_colour: Option<C>,
target: &mut D,
) where
C: RgbColor,
D: DrawTarget<Color = C>,
{
// 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::<i32>();
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::<i32>();
let y_pos_end: i32 = bounds.top_left.y
+ (graph_height - (((sample[1] - y_min) / y_range) * graph_height)).to_num::<i32>();
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;
}

60
src/input_task.rs Normal file
View File

@ -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!");
}
}
};
}
}

View File

@ -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<NoopRawMutex, RefCell<i2c::I2c<'static, I2C0, i2c::Blocking>>>;
@ -41,11 +43,11 @@ static SENSOR_DATA_SIGNAL: Signal<CriticalSectionRawMutex, scd4x::types::SensorD
/// Entrypoint
#[embassy_executor::main]
async fn main(spawner: Spawner) {
async fn main(spawner: Spawner) -> ! {
// 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<Spi0BusMutex> = 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 {

View File

@ -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
);