From 56f9c423e7f5a9ec635c717278f0648b2a35e9e2 Mon Sep 17 00:00:00 2001 From: Albin Chaboissier Date: Tue, 2 Dec 2025 23:08:29 +0100 Subject: [PATCH] Faster fill --- Cargo.lock | 203 +++++++++++++++++++++++++++++- Cargo.toml | 5 +- src/display.rs | 22 +++- src/graph.rs | 14 ++- src/main.rs | 293 +++++++++++++++++++++++++++++++++++++++++--- src/sampler.rs | 10 +- src/views/detail.rs | 51 ++++---- src/views/menu.rs | 26 ++-- 8 files changed, 547 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bc27e0..b041392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aht20-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c93801d1c9009ecef32e156413ef8f318478d1e7c7e38dc96654f412cf37827" +dependencies = [ + "bitflags 2.10.0", + "crc_all", + "embedded-hal-async", +] + [[package]] name = "aht20-driver" version = "2.0.0" @@ -16,6 +27,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d" +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "autocfg" version = "1.5.0" @@ -107,6 +124,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "co2-meter" version = "0.1.0" dependencies = [ + "aht20-async", "aht20-driver", "buoyant", "critical-section", @@ -118,6 +136,7 @@ dependencies = [ "embedded-graphics-framebuf", "embedded-hal-bus", "ens160", + "ens160-aq", "esp-alloc", "esp-backtrace", "esp-bootloader-esp-idf", @@ -128,6 +147,7 @@ dependencies = [ "libm", "mipidsi", "profont", + "static_cell", "tinybmp", ] @@ -143,6 +163,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" +[[package]] +name = "crc_all" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c46c1a17ebeef917714db3ae9a17bd2184f7e9977d8e020c6c8bcf59a28a6f1b" + [[package]] name = "critical-section" version = "1.2.0" @@ -266,7 +292,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -566,6 +592,20 @@ dependencies = [ "embedded-storage", ] +[[package]] +name = "embuild" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa4f198bb9152a55c0103efb83fa4edfcbb8625f4c9e94ae8ec8e23827c563" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "filetime", + "log", + "shlex", + "thiserror 1.0.69", +] + [[package]] name = "ens160" version = "0.6.1" @@ -577,6 +617,22 @@ dependencies = [ "maybe-async-cfg", ] +[[package]] +name = "ens160-aq" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bf02a6f1112f67da6a6638be8497415d7cf1ccba56d332889886108858f93" +dependencies = [ + "bitfield 0.14.0", + "byteorder", + "embedded-hal 1.0.0", + "embuild", + "libm", + "log", + "maybe-async-cfg", + "num-traits", +] + [[package]] name = "enumset" version = "1.1.10" @@ -893,6 +949,18 @@ dependencies = [ "vcell", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1085,6 +1153,17 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + [[package]] name = "linked_list_allocator" version = "0.10.5" @@ -1313,6 +1392,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "riscv" version = "0.15.0" @@ -1440,6 +1528,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.1" @@ -1467,6 +1561,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1538,13 +1641,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -1667,7 +1790,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1676,6 +1799,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1685,6 +1817,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index eaab5a4..8d757b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ embassy-executor = {version = "0.9.1", features = ["arch-riscv32"]} embassy-time = "0.5.0" embassy-sync = "0.7.2" embedded-graphics-framebuf = "0.5.0" +static_cell = "2.1.1" +ens160-aq = {version = "0.2.11"} +aht20-async = "1.0.0" @@ -50,5 +53,5 @@ debug = 2 debug-assertions = false incremental = false lto = 'fat' -opt-level = 's' +opt-level = 3 overflow-checks = false diff --git a/src/display.rs b/src/display.rs index 14f3022..7b0a921 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,16 +1,28 @@ use defmt::info; -use embedded_graphics::pixelcolor::Rgb565; -use embedded_graphics::prelude::DrawTarget; use embedded_hal_bus::spi::ExclusiveDevice; +use embedded_hal_bus::spi::NoDelay; +use esp_hal::Blocking; use esp_hal::delay::Delay; use esp_hal::gpio::Level; use esp_hal::gpio::Output; use esp_hal::gpio::OutputConfig; use esp_hal::gpio::OutputPin; use esp_hal::spi::master::Instance; +use esp_hal::spi::master::Spi; use esp_hal::time::Rate; +use mipidsi::Display; +use mipidsi::NoResetPin; use mipidsi::interface::SpiInterface; use mipidsi::models::ST7789; +use static_cell::StaticCell; + +pub type MainDisplay<'a> = Display< + SpiInterface<'a, ExclusiveDevice, Output<'a>, NoDelay>, Output<'a>>, + ST7789, + NoResetPin, +>; + +static SPI_BUFFER: StaticCell<[u8; 1024]> = StaticCell::new(); pub fn setup_display<'a>( spi: impl Instance + 'static, @@ -19,8 +31,7 @@ pub fn setup_display<'a>( cs: impl OutputPin + 'static, dc: impl OutputPin + 'static, timer: &mut Delay, - buffer: &mut [u8], -) -> impl DrawTarget { +) -> MainDisplay<'a> { let spi = esp_hal::spi::master::Spi::new( spi, esp_hal::spi::master::Config::default() @@ -36,10 +47,11 @@ pub fn setup_display<'a>( let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); // Define the display interface with no chip select + let spi_buffer = SPI_BUFFER.init([0u8; 1024]); let di = SpiInterface::new( spi_device, Output::new(dc, Level::Low, OutputConfig::default()), - buffer, + spi_buffer, ); info!("Display creating, initializing ..."); diff --git a/src/graph.rs b/src/graph.rs index ed8c371..7023287 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -11,7 +11,6 @@ use embedded_graphics::prelude::Size; use embedded_graphics::primitives::Line; use embedded_graphics::primitives::PrimitiveStyle; use embedded_graphics::text::Text; -use embedded_graphics::text::renderer::TextRenderer; use heapless::format; use profont::PROFONT_10_POINT; @@ -90,22 +89,27 @@ where for x in 0..size.width { // Start coloring from up to bottom let mut met_curve = false; + let mut cross_y = x % 7; - for y in 0..size.height { + let mut y = 0; + while y < size.height { let position = Point::new(x as i32, y as i32); - let pixel = target.pixel(position).unwrap(); let height_factor = map_float(y as f32, 0., size.height as f32, 1., 0.); let height_color = color_lut(height_factor, &DEFAULT_LUT); - if pixel == Rgb565::WHITE { + if target.pixel(position).unwrap() == Rgb565::WHITE { let _ = Pixel(position, height_color).draw(target); met_curve = true; - } else if met_curve && (x as i32 - y as i32) % 7 == 0 { + y += 1; + } else if met_curve { let _ = Pixel( position, rgb565_interpolate(height_color, Rgb565::BLACK, 1. - height_factor), ) .draw(target); + y += 7 - (y % 7); + } else { + y += 1; } } } diff --git a/src/main.rs b/src/main.rs index 2174b9c..38d5b64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,23 +6,36 @@ holding buffers for the duration of a data transfer." )] +use core::cell::RefCell; +use core::hint; + +use buoyant::primitives::ProposedDimension; +use buoyant::view::AsDrawable; +use critical_section::Mutex; use defmt::info; +use defmt::trace; use embassy_executor::Spawner; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::channel::Channel; +use embassy_time::Duration; use embassy_time::Timer; +use embedded_graphics::Drawable; use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::framebuffer::Framebuffer; +use embedded_graphics::framebuffer::buffer_size; use embedded_graphics::geometry::Dimensions; +use embedded_graphics::image::GetPixel; use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::pixelcolor::raw::LittleEndian; +use embedded_graphics::prelude::PixelColor; +use embedded_graphics::prelude::Primitive; use embedded_graphics::prelude::RgbColor; +use embedded_graphics::primitives::PrimitiveStyle; use embedded_graphics_framebuf::FrameBuf; +use embedded_graphics_framebuf::backends::FrameBufferBackend; use esp_hal::clock::CpuClock; use esp_hal::gpio::Input; use esp_hal::gpio::InputConfig; -use esp_hal::gpio::Level; -use esp_hal::gpio::Output; -use esp_hal::gpio::OutputConfig; use esp_hal::gpio::Pull; use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal::timer::timg::TimerGroup; @@ -31,18 +44,25 @@ use esp_rtos::main; extern crate alloc; extern crate esp_alloc; -// mod colors; -// mod graph; +mod colors; mod display; +mod graph; mod images; mod sampler; -//mod views; +mod views; esp_bootloader_esp_idf::esp_app_desc!(); use esp_backtrace as _; use esp_println as _; +use heapless::HistoryBuf; +use heapless::format; +use static_cell::StaticCell; +use crate::display::MainDisplay; +use crate::images::StaticImage; +use crate::sampler::History; use crate::sampler::Sample; +use crate::sampler::Sampler; pub enum ApplicationEvent { ButtonPress, @@ -51,14 +71,16 @@ pub enum ApplicationEvent { } static EVENT_CHANNEL: Channel = Channel::new(); +static MAIN_DISPLAY: Mutex>> = Mutex::new(RefCell::new(None)); +static SAMPLER: StaticCell = StaticCell::new(); #[main] -async fn main(spawner: Spawner) -> ! { +async fn main(spawner: Spawner) { esp_alloc::heap_allocator!(size: 32 * 1024); // generator version: 1.0.1 info!("Starting up."); - //images::prepare_images(); + images::prepare_images(); info!("Prepared images."); let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); @@ -70,7 +92,6 @@ async fn main(spawner: Spawner) -> ! { info!("Init done."); info!("Setting up display"); let mut timer = esp_hal::delay::Delay::new(); - let mut buffer = [0_u8; 512]; let mut display = display::setup_display( peripherals.SPI2, peripherals.GPIO4, @@ -78,7 +99,6 @@ async fn main(spawner: Spawner) -> ! { peripherals.GPIO0, peripherals.GPIO1, &mut timer, - &mut buffer, ); info!("Clearing screen"); @@ -86,20 +106,32 @@ async fn main(spawner: Spawner) -> ! { let fbuf_data = [Rgb565::new(0, 0, 0); 240 * 240]; let _ = display.fill_contiguous(&display.bounding_box(), fbuf_data); } + critical_section::with(|cs| { + MAIN_DISPLAY.borrow_ref_mut(cs).replace(display); + }); + // Setup button let btn = Input::new( peripherals.GPIO10, InputConfig::default().with_pull(Pull::Down), ); - spawner.spawn(button_listener(btn)); - spawner.spawn(event_handler()); + // Setup sampler + let sampler = SAMPLER.init(Sampler::new( + peripherals.I2C0, + peripherals.GPIO8, + peripherals.GPIO9, + &mut timer, + )); - loop {} + spawner.spawn(button_listener(btn)).unwrap(); + spawner.spawn(event_handler()).unwrap(); + spawner.spawn(sampler_task(sampler)).unwrap(); } #[embassy_executor::task] async fn button_listener(mut btn: Input<'static>) { + info!("Button listner task launched"); let sender = EVENT_CHANNEL.sender(); loop { btn.wait_for_rising_edge().await; @@ -111,17 +143,242 @@ async fn button_listener(mut btn: Input<'static>) { #[embassy_executor::task] async fn event_handler() { + info!("Event handler task launched"); let receiver = EVENT_CHANNEL.receiver(); - let mut state = false; + let mut display = critical_section::with(|cs| MAIN_DISPLAY.borrow_ref_mut(cs).take().unwrap()); + let mut fbuf = + Framebuffer::(240, 240) }>::new( + ); + + let mut last_5_mins: HistoryBuf = HistoryBuf::new(); + last_5_mins.write(Sample::zero()); + let mut redraw = true; + let mut current_view = ViewState::Main; + loop { + if redraw { + redraw = false; + let mut draw_graph = None; + let tendencies = Tendencies::from_history(&last_5_mins); + let _ = fbuf + .bounding_box() + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut fbuf); + match current_view { + ViewState::Main => { + let _ = views::menu::menu_view(*last_5_mins.last().unwrap(), tendencies) + .as_drawable(fbuf.bounding_box().size, Rgb565::WHITE) + .draw(&mut fbuf); + } + ViewState::TemperatureGraph => draw_graph = Some(MeasurementType::Temperature), + ViewState::HumidityGraph => draw_graph = Some(MeasurementType::Humidity), + ViewState::ECo2Graph => draw_graph = Some(MeasurementType::ECo2), + ViewState::TVocGraph => draw_graph = Some(MeasurementType::TVoc), + } + + if let Some(graph_type) = draw_graph { + info!("Drawing graph"); + views::detail::detailed(&last_5_mins, tendencies, graph_type, &mut fbuf); + info!("Drew graph"); + } + + let _ = display.fill_contiguous( + &fbuf.bounding_box(), + fbuf.data() + .chunks(2) + .map(|x| unsafe { core::mem::transmute::<_, Rgb565>([x[0], x[1]]) }), + ); + } let event = receiver.receive().await; match event { ApplicationEvent::ButtonPress => { - state = !state; - let _ = fbuf.clear(if state { Rgb565::WHITE } else { Rgb565::BLACK }); - let _ = display.fill_contiguous(&display.bounding_box(), fbuf.data.iter().copied()); + current_view = current_view.next(); + redraw = true; } + ApplicationEvent::NewSample(x) => { + last_5_mins.write(x); + redraw = true; + } + _ => {} } } } + +#[derive(Clone, Copy)] +pub enum ViewState { + Main, + TemperatureGraph, + HumidityGraph, + ECo2Graph, + TVocGraph, +} + +impl ViewState { + pub fn next(&self) -> Self { + match self { + ViewState::Main => ViewState::TemperatureGraph, + ViewState::TemperatureGraph => ViewState::HumidityGraph, + ViewState::HumidityGraph => ViewState::ECo2Graph, + ViewState::ECo2Graph => ViewState::TVocGraph, + ViewState::TVocGraph => ViewState::Main, + } + } +} + +const LOW_PASS_LENGTH: usize = 10; + +#[embassy_executor::task] +async fn sampler_task(sampler: &'static mut Sampler<'static>) { + info!("Sampler task launched"); + let sender = EVENT_CHANNEL.sender(); + sender + .send(ApplicationEvent::NewSample(Sample::zero())) + .await; + let mut low_pass: HistoryBuf = HistoryBuf::new(); + let mut delay = esp_hal::delay::Delay::new(); + let mut count = 0; + + loop { + Timer::after(Duration::from_millis(500)).await; + let sample = sampler.sample(&mut delay); + low_pass.write(sample); + count += 1; + + if count >= 2 { + sender.send(ApplicationEvent::NewSample(sample)).await; + count = 0; + } + } +} + +#[derive(Clone, Copy)] +pub struct Tendencies { + temperature: Tendency, + humidity: Tendency, + eco2: Tendency, + tvoc: Tendency, +} + +impl Tendencies { + pub fn from_history(history: &HistoryBuf) -> Self { + let mut iter = history.oldest_ordered().rev().copied().take(5); + let len = history.len().min(5); + + if len <= 1 { + return Tendencies { + temperature: Tendency::Steady, + humidity: Tendency::Steady, + eco2: Tendency::Steady, + tvoc: Tendency::Steady, + }; + } + + let mut last = iter.next().unwrap(); + let mut avg_slope = Sample::zero(); + for x in iter { + avg_slope = avg_slope + (last - x); + last = x; + } + + avg_slope = avg_slope * (1. / len as f32); + + const TEMPERATURE_TENDENCY_TRESHOLD: f32 = 0.3; + const HUMIDITY_TENDENCY_TRESHOLD: f32 = 0.3; + const ECO2_TENDENCY_TRESHOLD: f32 = 50.; + const TVOC_TENDENCY_TRESHOLD: f32 = 50.; + Tendencies { + temperature: if avg_slope.temperature > TEMPERATURE_TENDENCY_TRESHOLD { + Tendency::Rising + } else if avg_slope.temperature < -TEMPERATURE_TENDENCY_TRESHOLD { + Tendency::Falling + } else { + Tendency::Steady + }, + humidity: if avg_slope.humidity > HUMIDITY_TENDENCY_TRESHOLD { + Tendency::Rising + } else if avg_slope.humidity < -HUMIDITY_TENDENCY_TRESHOLD { + Tendency::Falling + } else { + Tendency::Steady + }, + eco2: if avg_slope.eco2 > ECO2_TENDENCY_TRESHOLD { + Tendency::Rising + } else if avg_slope.eco2 < -ECO2_TENDENCY_TRESHOLD { + Tendency::Falling + } else { + Tendency::Steady + }, + tvoc: if avg_slope.tvoc > TVOC_TENDENCY_TRESHOLD { + Tendency::Rising + } else if avg_slope.tvoc < -TVOC_TENDENCY_TRESHOLD { + Tendency::Falling + } else { + Tendency::Steady + }, + } + } +} + +#[derive(Clone, Copy)] +pub enum Tendency { + Rising, + Steady, + Falling, +} + +impl Tendency { + pub fn get_corresponding_icon(&self) -> &'static StaticImage { + match self { + Self::Rising => &images::TENDENCY_RISING, + Self::Steady => &images::TENDENCY_STEADY, + Self::Falling => &images::TENDENCY_FALLING, + } + } +} + +#[derive(Clone, Copy)] +pub enum MeasurementType { + Temperature, + Humidity, + ECo2, + TVoc, +} + +impl MeasurementType { + pub fn get_corresponding_icon(&self) -> &'static StaticImage { + match self { + MeasurementType::Temperature => &images::TEMPERATURE_ICON, + MeasurementType::Humidity => &images::HUMIDITY_ICON, + MeasurementType::ECo2 => &images::CO2_ICON, + MeasurementType::TVoc => &images::VOC_ICON, + } + } + + pub fn get_corresponding_unit_string(&self) -> &'static str { + match self { + MeasurementType::Temperature => "C", + MeasurementType::Humidity => "%", + MeasurementType::ECo2 => "ppm", + MeasurementType::TVoc => "ppb", + } + } + + pub fn get_value_str(&self, sample: Sample) -> heapless::String<16> { + match self { + MeasurementType::Temperature => format!(16; "{:.1}", sample.temperature).unwrap(), + MeasurementType::Humidity => format!(16; "{:.1}", sample.humidity).unwrap(), + MeasurementType::ECo2 => format!(16; "{}", sample.eco2 as u32).unwrap(), + MeasurementType::TVoc => format!(16; "{}", sample.tvoc as u32).unwrap(), + } + } + + pub fn get_tendency(&self, tendencies: Tendencies) -> Tendency { + match self { + MeasurementType::Temperature => tendencies.temperature, + MeasurementType::Humidity => tendencies.humidity, + MeasurementType::ECo2 => tendencies.eco2, + MeasurementType::TVoc => tendencies.tvoc, + } + } +} diff --git a/src/sampler.rs b/src/sampler.rs index ffe7619..37a627c 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -5,26 +5,19 @@ use core::ops::Sub; use aht20_driver::AHT20; use alloc::rc::Rc; -use alloc::vec::Vec; use core::default::Default; use embedded_hal_bus::i2c::RcDevice; +//use ens160_aq::Ens160; use ens160::Ens160; use esp_hal::Blocking; -use esp_hal::DriverMode; use esp_hal::delay::Delay; use esp_hal::gpio::interconnect::PeripheralOutput; -use esp_hal::i2c; use esp_hal::i2c::master::I2c; use esp_hal::i2c::master::Instance; -use esp_hal::peripherals; -use esp_hal::peripherals::Peripherals; use esp_hal::time::Duration; use esp_hal::time::Instant; -use heapless::Deque; use heapless::HistoryBuf; -use crate::sampler; - pub struct Sampler<'a> { ens160: Ens160>>, aht20: aht20_driver::AHT20Initialized>>, @@ -133,6 +126,7 @@ impl<'a> Sampler<'a> { pub fn sample(&mut self, timer: &mut Delay) -> Sample { let aht20_measurement = self.aht20.measure(timer).unwrap(); + Sample { temperature: aht20_measurement.temperature, humidity: aht20_measurement.humidity, diff --git a/src/views/detail.rs b/src/views/detail.rs index 990e37a..6cb4f94 100644 --- a/src/views/detail.rs +++ b/src/views/detail.rs @@ -17,28 +17,29 @@ use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::pixelcolor::raw::LittleEndian; use embedded_graphics::prelude::DrawTarget; use embedded_graphics::prelude::RgbColor; -use esp_hal::tsens::Temperature; +use heapless::HistoryBuf; use profont::PROFONT_18_POINT; use profont::PROFONT_24_POINT; -use crate::AppState; -use crate::MenuIndicatorType; -use crate::Tendency; +use crate::MeasurementType; +use crate::Tendencies; use crate::colors::FRAME_STROKE_COLOR; use crate::colors::MAIN_TEXT_COLOR; use crate::colors::SUB_TEXT_COLOR; use crate::graph::graph_data; use crate::graph::max_indicator; use crate::graph::min_indicator; -use crate::tendency_indicator; +use crate::sampler::Sample; use crate::views::icon::icon_box_view; +use crate::views::menu::tendency_indicator; -pub fn detailed + GetPixel>( - app_state: AppState, - indicator: MenuIndicatorType, +pub fn detailed + GetPixel, const N: usize>( + history: &HistoryBuf, + tendencies: Tendencies, + indicator: MeasurementType, target: &mut T, ) { - let _ = detailed_view_top(indicator, app_state) + let _ = detailed_view_top(indicator, tendencies, *history.last().unwrap()) .as_drawable(Size::new(240, 240), Rgb565::WHITE) .draw(target); @@ -50,19 +51,15 @@ pub fn detailed + GetPixel>( { 240 - 53 }, { buffer_size::(240, 240) }, >::new(); - let iter = app_state - .history - .min5 - .oldest_ordered() - .map(|x| match indicator { - MenuIndicatorType::Temperature => x.temperature, - MenuIndicatorType::Humidity => x.humidity, - MenuIndicatorType::Co2 => x.eco2, - MenuIndicatorType::Voc => x.tvoc, - }); - graph_data(iter.clone(), app_state.history.min5.len(), &mut graph_fb); - min_indicator(iter.clone(), app_state.history.min5.len(), &mut graph_fb); - max_indicator(iter.clone(), app_state.history.min5.len(), &mut graph_fb); + let iter = history.oldest_ordered().map(|x| match indicator { + MeasurementType::Temperature => x.temperature, + MeasurementType::Humidity => x.humidity, + MeasurementType::ECo2 => x.eco2, + MeasurementType::TVoc => x.tvoc, + }); + graph_data(iter.clone(), history.len(), &mut graph_fb); + min_indicator(iter.clone(), history.len(), &mut graph_fb); + max_indicator(iter.clone(), history.len(), &mut graph_fb); let img_raw = ImageRaw::::new(graph_fb.data(), 240); let image = embedded_graphics::image::Image::new( @@ -72,14 +69,18 @@ pub fn detailed + GetPixel>( let _ = image.draw(target); } -pub fn detailed_view_top(indicator: MenuIndicatorType, app_state: AppState) -> impl View { +pub fn detailed_view_top( + indicator: MeasurementType, + tendencies: Tendencies, + sample: Sample, +) -> impl View { VStack::new(( // Header HStack::new(( icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()), Spacer::default().flex_frame().with_max_width(10), - tendency_indicator(indicator.get_tendency(&app_state)), - Text::new(indicator.get_value_str(&app_state), &PROFONT_24_POINT) + tendency_indicator(indicator.get_tendency(tendencies)), + Text::new(indicator.get_value_str(sample), &PROFONT_24_POINT) .foreground_color(MAIN_TEXT_COLOR), Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT) .foreground_color(SUB_TEXT_COLOR) diff --git a/src/views/menu.rs b/src/views/menu.rs index a2515fc..aaabbbb 100644 --- a/src/views/menu.rs +++ b/src/views/menu.rs @@ -7,8 +7,8 @@ use embedded_graphics::prelude::*; use profont::PROFONT_18_POINT; use profont::PROFONT_24_POINT; -use crate::AppState; -use crate::MenuIndicatorType; +use crate::MeasurementType; +use crate::Tendencies; use crate::Tendency; use crate::colors::FRAME_BACKGROUD_COLOR; use crate::colors::FRAME_STROKE; @@ -16,18 +16,19 @@ use crate::colors::FRAME_STROKE_COLOR; use crate::colors::MAIN_TEXT_COLOR; use crate::colors::SUB_TEXT_COLOR; use crate::get_image; +use crate::sampler::Sample; use crate::views::icon::icon_box_view; -pub fn menu_view(app_state: AppState) -> impl View { +pub fn menu_view(sample: Sample, tendencies: Tendencies) -> impl View { VStack::new(( HStack::new(( - main_menu_indicator(MenuIndicatorType::Temperature, app_state), - main_menu_indicator(MenuIndicatorType::Humidity, app_state), + main_menu_indicator(MeasurementType::Temperature, tendencies, sample), + main_menu_indicator(MeasurementType::Humidity, tendencies, sample), )) .with_spacing(2), HStack::new(( - main_menu_indicator(MenuIndicatorType::Co2, app_state), - main_menu_indicator(MenuIndicatorType::Voc, app_state), + main_menu_indicator(MeasurementType::ECo2, tendencies, sample), + main_menu_indicator(MeasurementType::TVoc, tendencies, sample), )) .with_spacing(2), )) @@ -35,8 +36,9 @@ pub fn menu_view(app_state: AppState) -> impl View { } fn main_menu_indicator( - indicator_type: MenuIndicatorType, - app_state: AppState, + indicator_type: MeasurementType, + tendencies: Tendencies, + sample: Sample, ) -> impl View { Rectangle .corner_radius(5) @@ -55,8 +57,8 @@ fn main_menu_indicator( )), HStack::new(( Spacer::default(), - tendency_indicator(indicator_type.get_tendency(&app_state)), - Text::new(indicator_type.get_value_str(&app_state), &PROFONT_24_POINT) + tendency_indicator(indicator_type.get_tendency(tendencies)), + Text::new(indicator_type.get_value_str(sample), &PROFONT_24_POINT) .foreground_color(MAIN_TEXT_COLOR), Text::new( indicator_type.get_corresponding_unit_string(), @@ -76,7 +78,7 @@ fn main_menu_indicator( }) } -fn tendency_indicator(tendency: Tendency) -> impl View { +pub fn tendency_indicator(tendency: Tendency) -> impl View { HStack::new(( Image::new(get_image!(tendency.get_corresponding_icon())) .flex_frame()