From 9925499eff733a692c3239a60a8e997ebd1f6eb0 Mon Sep 17 00:00:00 2001 From: Albin Chaboissier Date: Tue, 2 Dec 2025 20:43:50 +0100 Subject: [PATCH] Embassy --- Cargo.lock | 109 ++++++++++++ Cargo.toml | 5 + main.rs | 429 +++++++++++++++++++++++++++++++++++++++++++++ src/display.rs | 52 ++++++ src/main.rs | 461 +++++++++---------------------------------------- 5 files changed, 675 insertions(+), 381 deletions(-) create mode 100644 main.rs create mode 100644 src/display.rs diff --git a/Cargo.lock b/Cargo.lock index 1c798c5..3bc27e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,11 @@ dependencies = [ "buoyant", "critical-section", "defmt 1.0.1", + "embassy-executor", + "embassy-sync 0.7.2", + "embassy-time", "embedded-graphics", + "embedded-graphics-framebuf", "embedded-hal-bus", "ens160", "esp-alloc", @@ -119,6 +123,7 @@ dependencies = [ "esp-bootloader-esp-idf", "esp-hal", "esp-println", + "esp-rtos", "heapless 0.9.2", "libm", "mipidsi", @@ -310,6 +315,36 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embassy-executor" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b" +dependencies = [ + "critical-section", + "document-features", + "embassy-executor-macros", + "embassy-executor-timer-queue", +] + +[[package]] +name = "embassy-executor-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "embassy-executor-timer-queue" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" + [[package]] name = "embassy-futures" version = "0.1.2" @@ -357,6 +392,41 @@ dependencies = [ "heapless 0.8.0", ] +[[package]] +name = "embassy-time" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65" +dependencies = [ + "cfg-if", + "critical-section", + "document-features", + "embassy-time-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-utils" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" +dependencies = [ + "embassy-executor-timer-queue", + "heapless 0.8.0", +] + [[package]] name = "embedded-can" version = "0.4.1" @@ -366,6 +436,15 @@ dependencies = [ "nb 1.1.0", ] +[[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]] name = "embedded-graphics" version = "0.8.1" @@ -389,6 +468,16 @@ dependencies = [ "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]] name = "embedded-hal" version = "0.2.7" @@ -691,6 +780,26 @@ dependencies = [ "esp-metadata-generated", ] +[[package]] +name = "esp-rtos" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ec711c8d06e79c67b75d01595539e86b0aac209643af98ca87a12250428b3" +dependencies = [ + "cfg-if", + "document-features", + "embassy-executor", + "embassy-sync 0.7.2", + "embassy-time-driver", + "embassy-time-queue-utils", + "esp-config", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-sync", + "portable-atomic", +] + [[package]] name = "esp-sync" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index bd1bf02..eaab5a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ version = "0.1.0" [dependencies] esp-hal = { version = "1.0.0", features = ["defmt", "esp32c3", "unstable"] } +esp-rtos = {version = "0.2.0", features = ["esp32c3", "embassy"]} defmt = "1.0.1" @@ -31,6 +32,10 @@ buoyant = "0.5.3" heapless = "0.9.2" tinybmp = "0.6.0" libm = "0.2.15" +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" diff --git a/main.rs b/main.rs new file mode 100644 index 0000000..723c827 --- /dev/null +++ b/main.rs @@ -0,0 +1,429 @@ +#![no_std] +#![no_main] +#![deny( + clippy::mem_forget, + reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ + holding buffers for the duration of a data transfer." +)] +#![allow(unreachable_code)] + +mod colors; +mod graph; +mod images; +mod sampler; +mod views; + +use buoyant::primitives::Pixel; +use buoyant::primitives::Size; +use buoyant::primitives::geometry::Rectangle; +use buoyant::view::AsDrawable; +use buoyant::view::Image; +use buoyant::view::ViewExt; +use core::cell::RefCell; +use core::default::Default; +use core::iter::Iterator; +use core::ops::Sub; +use critical_section::Mutex; +use defmt::info; +use embedded_graphics::Drawable; +use embedded_graphics::framebuffer::Framebuffer; +use embedded_graphics::framebuffer::buffer_size; +use embedded_graphics::image::GetPixel; +use embedded_graphics::image::ImageRaw; +use embedded_graphics::pixelcolor::raw::LittleEndian; +use embedded_graphics::prelude::Dimensions; +use embedded_graphics::prelude::DrawTarget; +use embedded_graphics::prelude::Point; +use embedded_graphics::prelude::Primitive; +use embedded_graphics::prelude::RgbColor; +use embedded_graphics::primitives::PrimitiveStyle; +use embedded_hal_bus::spi::ExclusiveDevice; +use esp_hal::clock::CpuClock; +use esp_hal::delay::Delay; +use esp_hal::gpio::Input; +use esp_hal::gpio::InputConfig; +use esp_hal::gpio::Io; +use esp_hal::gpio::Level; +use esp_hal::gpio::Output; +use esp_hal::gpio::OutputConfig; +use esp_hal::gpio::Pull; +use esp_hal::handler; +use esp_hal::peripherals; +use esp_hal::time::Rate; +use heapless::format; +use mipidsi::interface::SpiInterface; +use mipidsi::models::ST7789; + +use buoyant::view::HStack; +use buoyant::view::Spacer; +use buoyant::view::View; +use core::env; +use embedded_graphics::pixelcolor::Rgb565; +use esp_backtrace as _; +use esp_hal::main; +use esp_println as _; + +use crate::colors::BACKGROUND_COLOR; +use crate::graph::graph_data; +use crate::graph::max_indicator; +use crate::graph::min_indicator; +use crate::images::StaticImage; +use crate::sampler::History; +use crate::sampler::Sample; +use crate::sampler::Sampler; +use crate::views::detail; + +extern crate alloc; + +// This creates a default app-descriptor required by the esp-idf bootloader. +// For more information see: +esp_bootloader_esp_idf::esp_app_desc!(); + +static INPUT_BUTTON: Mutex>> = Mutex::new(RefCell::new(None)); +static BUTTON_PRESSED: Mutex> = Mutex::new(RefCell::new(false)); + +#[main] +fn main() -> ! { + // generator version: 1.0.1 + info!("Starting up."); + images::prepare_images(); + info!("Prepared images."); + + esp_alloc::heap_allocator!(size: 32 * 1024); + let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); + let peripherals = esp_hal::init(config); + let mut timer = Delay::new(); + let mut io = Io::new(peripherals.IO_MUX); + info!("Init done."); + + let spi = esp_hal::spi::master::Spi::new( + peripherals.SPI2, + esp_hal::spi::master::Config::default() + .with_mode(esp_hal::spi::Mode::_0) + .with_frequency(Rate::from_mhz(80)), + ) + .unwrap() + .with_sck(peripherals.GPIO4) + .with_mosi(peripherals.GPIO6); + + let mut cs_output = Output::new(peripherals.GPIO0, Level::High, OutputConfig::default()); + cs_output.set_high(); + let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); + + let mut buffer = [0_u8; 512]; + + // Define the display interface with no chip select + let di = SpiInterface::new( + spi_device, + Output::new(peripherals.GPIO1, Level::Low, OutputConfig::default()), + &mut buffer, + ); + info!("Display creating, initializing ..."); + + let mut display = mipidsi::Builder::new(ST7789, di) + .invert_colors(mipidsi::options::ColorInversion::Inverted) + .init(&mut timer) + .unwrap(); + info!("Initialized"); + + let mut fb = + Framebuffer::(240, 240) }>::new( + ); + + // views::menu::menu_view() + // .as_drawable(Size::new(240, 240), Rgb565::WHITE) + // .draw(&mut fb) + // .unwrap(); + + // embedded_graphics::image::Image::new(get_image!(images::HUMIDITY_ICON), Point::zero()) + // .draw(&mut display) + // .unwrap(); + + info!("Creating sampler"); + let mut sampler = Sampler::new( + peripherals.I2C0, + peripherals.GPIO8, + peripherals.GPIO9, + &mut timer, + ); + let _ = sampler.sample(&mut timer); + info!("Sensor initialized"); + + // Input button interrupt + { + esp_hal::interrupt::enable( + peripherals::Interrupt::GPIO, + esp_hal::interrupt::Priority::Priority1, + ) + .unwrap(); + let mut input_button = Input::new( + peripherals.GPIO10, + InputConfig::default().with_pull(Pull::Up), + ); + + io.set_interrupt_handler(interrupt_handler); + critical_section::with(|cs| { + input_button.listen(esp_hal::gpio::Event::FallingEdge); + INPUT_BUTTON.borrow_ref_mut(cs).replace(input_button); + }); + } + info!("Setup interrupts"); + + let mut history = History::new(&mut sampler, &mut timer); + let mut view_state = ViewState::Main; + let mut x = 0; + + loop { + // input state + let button_pressed = critical_section::with(|cs| { + let val = *BUTTON_PRESSED.borrow(cs).borrow(); + *BUTTON_PRESSED.borrow_ref_mut(cs) = false; + val + }); + + if button_pressed { + view_state = view_state.circulate(); + info!("Btn pressed"); + } + + if false && (history.update(&mut sampler, &mut timer) || button_pressed) { + // let iter = history.min5.oldest_ordered().map(|x| x.eco2); + // embedded_graphics::primitives::Rectangle::new(Point::zero(), fb.bounding_box().size) + // .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + // .draw(&mut fb) + // .unwrap(); + // graph_data(iter.clone(), history.min5.len(), &mut fb); + // min_indicator(iter.clone(), history.min5.len(), &mut fb); + // max_indicator(iter.clone(), history.min5.len(), &mut fb); + // + // let img_raw = ImageRaw::::new(fb.data(), 240); + // let image = embedded_graphics::image::Image::new(&img_raw, Point::zero()); + // image.draw(&mut display).unwrap(); + + let app_state = AppState { + sample: *history.min5.last().unwrap(), + history: &history, + tendencies: Tendencies::from_history(&history), + }; + display + .bounding_box() + .into_styled(PrimitiveStyle::with_fill(BACKGROUND_COLOR)) + .draw(&mut fb) + .unwrap(); + view_state.draw_view(&mut fb, app_state); + let img_raw = ImageRaw::::new(fb.data(), 240); + let image = embedded_graphics::image::Image::new(&img_raw, Point::zero()); + image.draw(&mut display).unwrap(); + } + } + + // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0/examples/src/bin +} + +#[derive(Clone, Copy)] +pub struct Tendencies { + temperature: Tendency, + humidity: Tendency, + eco2: Tendency, + tvoc: Tendency, +} + +impl Tendencies { + pub fn from_history(history: &History) -> Self { + let mut iter = history.min5.oldest_ordered().rev().copied().take(5); + let len = history.min5.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 struct AppState<'a> { + sample: Sample, + tendencies: Tendencies, + history: &'a History, +} + +#[derive(Clone, Copy)] +pub enum ViewState { + Main, + GraphTemperature, + GraphHumidity, + GraphECo2, + GraphTvoc, +} + +impl ViewState { + pub fn circulate(&self) -> Self { + match self { + ViewState::Main => ViewState::GraphTemperature, + ViewState::GraphTemperature => ViewState::GraphHumidity, + ViewState::GraphHumidity => ViewState::GraphECo2, + ViewState::GraphECo2 => ViewState::GraphTvoc, + ViewState::GraphTvoc => ViewState::Main, + } + } + + pub fn draw_view + GetPixel>( + &self, + target: &mut T, + app_state: AppState<'_>, + ) { + match self { + ViewState::Main => { + let _ = views::menu::menu_view(app_state) + .as_drawable(Size::new(240, 240), Rgb565::WHITE) + .draw(target); + } + ViewState::GraphTemperature => { + detail::detailed(app_state, MenuIndicatorType::Temperature, target) + } + ViewState::GraphHumidity => { + detail::detailed(app_state, MenuIndicatorType::Humidity, target) + } + ViewState::GraphECo2 => detail::detailed(app_state, MenuIndicatorType::Co2, target), + ViewState::GraphTvoc => detail::detailed(app_state, MenuIndicatorType::Voc, target), + } + } +} + +#[derive(Clone, Copy)] +pub enum MenuIndicatorType { + Temperature, + Humidity, + Co2, + Voc, +} + +impl MenuIndicatorType { + pub fn get_corresponding_icon(&self) -> &'static StaticImage { + match self { + MenuIndicatorType::Temperature => &images::TEMPERATURE_ICON, + MenuIndicatorType::Humidity => &images::HUMIDITY_ICON, + MenuIndicatorType::Co2 => &images::CO2_ICON, + MenuIndicatorType::Voc => &images::VOC_ICON, + } + } + + pub fn get_corresponding_unit_string(&self) -> &'static str { + match self { + MenuIndicatorType::Temperature => "C", + MenuIndicatorType::Humidity => "%", + MenuIndicatorType::Co2 => "ppm", + MenuIndicatorType::Voc => "ppb", + } + } + + pub fn get_value_str(&self, app_state: &AppState) -> heapless::String<16> { + match self { + MenuIndicatorType::Temperature => { + format!(16; "{:.1}", app_state.sample.temperature).unwrap() + } + MenuIndicatorType::Humidity => format!(16; "{:.1}", app_state.sample.humidity).unwrap(), + MenuIndicatorType::Co2 => format!(16; "{}", app_state.sample.eco2 as u32).unwrap(), + MenuIndicatorType::Voc => format!(16; "{}", app_state.sample.tvoc as u32).unwrap(), + } + } + + pub fn get_tendency(&self, app_state: &AppState) -> Tendency { + match self { + MenuIndicatorType::Temperature => app_state.tendencies.temperature, + MenuIndicatorType::Humidity => app_state.tendencies.humidity, + MenuIndicatorType::Co2 => app_state.tendencies.eco2, + MenuIndicatorType::Voc => app_state.tendencies.tvoc, + } + } +} + +#[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, + } + } +} + +fn tendency_indicator(tendency: Tendency) -> impl View { + HStack::new(( + Image::new(get_image!(tendency.get_corresponding_icon())) + .flex_frame() + .with_min_size(10, 20) + .with_max_size(10, 20), + Spacer::default(), + )) + .flex_frame() + .with_max_width(15) +} + +#[handler] +fn interrupt_handler() { + critical_section::with(|cs| { + let mut button = INPUT_BUTTON.borrow_ref_mut(cs); + let Some(button) = button.as_mut() else { + return; + }; + + *BUTTON_PRESSED.borrow_ref_mut(cs) = true; + button.clear_interrupt(); + }); +} diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..14f3022 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,52 @@ +use defmt::info; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::DrawTarget; +use embedded_hal_bus::spi::ExclusiveDevice; +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::time::Rate; +use mipidsi::interface::SpiInterface; +use mipidsi::models::ST7789; + +pub fn setup_display<'a>( + spi: impl Instance + 'static, + sck: impl OutputPin + 'static, + mosi: impl OutputPin + 'static, + cs: impl OutputPin + 'static, + dc: impl OutputPin + 'static, + timer: &mut Delay, + buffer: &mut [u8], +) -> impl DrawTarget { + let spi = esp_hal::spi::master::Spi::new( + spi, + esp_hal::spi::master::Config::default() + .with_mode(esp_hal::spi::Mode::_0) + .with_frequency(Rate::from_mhz(80)), + ) + .unwrap() + .with_sck(sck) + .with_mosi(mosi); + + let mut cs_output = Output::new(cs, Level::High, OutputConfig::default()); + cs_output.set_high(); + let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); + + // Define the display interface with no chip select + let di = SpiInterface::new( + spi_device, + Output::new(dc, Level::Low, OutputConfig::default()), + buffer, + ); + info!("Display creating, initializing ..."); + + let display = mipidsi::Builder::new(ST7789, di) + .invert_colors(mipidsi::options::ColorInversion::Inverted) + .init(timer) + .unwrap(); + info!("Initialized"); + display +} diff --git a/src/main.rs b/src/main.rs index dd1d02f..2174b9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,424 +5,123 @@ reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ holding buffers for the duration of a data transfer." )] -#![allow(unreachable_code)] -mod colors; -mod graph; -mod images; -mod sampler; -mod views; - -use buoyant::primitives::Pixel; -use buoyant::primitives::Size; -use buoyant::primitives::geometry::Rectangle; -use buoyant::view::AsDrawable; -use buoyant::view::Image; -use buoyant::view::ViewExt; -use core::cell::RefCell; -use core::default::Default; -use core::iter::Iterator; -use core::ops::Sub; -use critical_section::Mutex; use defmt::info; -use embedded_graphics::Drawable; -use embedded_graphics::framebuffer::Framebuffer; -use embedded_graphics::framebuffer::buffer_size; -use embedded_graphics::image::GetPixel; -use embedded_graphics::image::ImageRaw; -use embedded_graphics::pixelcolor::raw::LittleEndian; -use embedded_graphics::prelude::Dimensions; -use embedded_graphics::prelude::DrawTarget; -use embedded_graphics::prelude::Point; -use embedded_graphics::prelude::Primitive; +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::Timer; +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::geometry::Dimensions; +use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::RgbColor; -use embedded_graphics::primitives::PrimitiveStyle; -use embedded_hal_bus::spi::ExclusiveDevice; +use embedded_graphics_framebuf::FrameBuf; use esp_hal::clock::CpuClock; -use esp_hal::delay::Delay; use esp_hal::gpio::Input; use esp_hal::gpio::InputConfig; -use esp_hal::gpio::Io; use esp_hal::gpio::Level; use esp_hal::gpio::Output; use esp_hal::gpio::OutputConfig; use esp_hal::gpio::Pull; -use esp_hal::handler; -use esp_hal::peripherals; -use esp_hal::time::Rate; -use heapless::format; -use mipidsi::interface::SpiInterface; -use mipidsi::models::ST7789; - -use buoyant::view::HStack; -use buoyant::view::Spacer; -use buoyant::view::View; -use core::env; -use embedded_graphics::pixelcolor::Rgb565; -use esp_backtrace as _; -use esp_hal::main; -use esp_println as _; - -use crate::colors::BACKGROUND_COLOR; -use crate::graph::graph_data; -use crate::graph::max_indicator; -use crate::graph::min_indicator; -use crate::images::StaticImage; -use crate::sampler::History; -use crate::sampler::Sample; -use crate::sampler::Sampler; -use crate::views::detail; +use esp_hal::interrupt::software::SoftwareInterruptControl; +use esp_hal::timer::timg::TimerGroup; +use esp_rtos::main; extern crate alloc; +extern crate esp_alloc; + +// mod colors; +// mod graph; +mod display; +mod images; +mod sampler; +//mod views; -// This creates a default app-descriptor required by the esp-idf bootloader. -// For more information see: esp_bootloader_esp_idf::esp_app_desc!(); +use esp_backtrace as _; +use esp_println as _; -static INPUT_BUTTON: Mutex>> = Mutex::new(RefCell::new(None)); -static BUTTON_PRESSED: Mutex> = Mutex::new(RefCell::new(false)); +use crate::sampler::Sample; + +pub enum ApplicationEvent { + ButtonPress, + LongButtonPress, + NewSample(Sample), +} + +static EVENT_CHANNEL: Channel = Channel::new(); #[main] -fn main() -> ! { +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."); - esp_alloc::heap_allocator!(size: 32 * 1024); let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); - let mut timer = Delay::new(); - let mut io = Io::new(peripherals.IO_MUX); + let timg0 = TimerGroup::new(peripherals.TIMG0); + let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timg0.timer0, software_interrupt.software_interrupt0); + info!("Init done."); - - let spi = esp_hal::spi::master::Spi::new( - peripherals.SPI2, - esp_hal::spi::master::Config::default() - .with_mode(esp_hal::spi::Mode::_0) - .with_frequency(Rate::from_mhz(80)), - ) - .unwrap() - .with_sck(peripherals.GPIO4) - .with_mosi(peripherals.GPIO6); - - let mut cs_output = Output::new(peripherals.GPIO0, Level::High, OutputConfig::default()); - cs_output.set_high(); - let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap(); - + info!("Setting up display"); + let mut timer = esp_hal::delay::Delay::new(); let mut buffer = [0_u8; 512]; - - // Define the display interface with no chip select - let di = SpiInterface::new( - spi_device, - Output::new(peripherals.GPIO1, Level::Low, OutputConfig::default()), + let mut display = display::setup_display( + peripherals.SPI2, + peripherals.GPIO4, + peripherals.GPIO6, + peripherals.GPIO0, + peripherals.GPIO1, + &mut timer, &mut buffer, ); - info!("Display creating, initializing ..."); - let mut display = mipidsi::Builder::new(ST7789, di) - .invert_colors(mipidsi::options::ColorInversion::Inverted) - .init(&mut timer) - .unwrap(); - info!("Initialized"); - - let mut fb = - Framebuffer::(240, 240) }>::new( - ); - - // views::menu::menu_view() - // .as_drawable(Size::new(240, 240), Rgb565::WHITE) - // .draw(&mut fb) - // .unwrap(); - - // embedded_graphics::image::Image::new(get_image!(images::HUMIDITY_ICON), Point::zero()) - // .draw(&mut display) - // .unwrap(); - - info!("Creating sampler"); - let mut sampler = Sampler::new( - peripherals.I2C0, - peripherals.GPIO8, - peripherals.GPIO9, - &mut timer, - ); - let _ = sampler.sample(&mut timer); - info!("Sensor initialized"); - - // Input button interrupt + info!("Clearing screen"); { - esp_hal::interrupt::enable( - peripherals::Interrupt::GPIO, - esp_hal::interrupt::Priority::Priority1, - ) - .unwrap(); - let mut input_button = Input::new( - peripherals.GPIO10, - InputConfig::default().with_pull(Pull::Up), - ); - - io.set_interrupt_handler(interrupt_handler); - critical_section::with(|cs| { - input_button.listen(esp_hal::gpio::Event::FallingEdge); - INPUT_BUTTON.borrow_ref_mut(cs).replace(input_button); - }); + let fbuf_data = [Rgb565::new(0, 0, 0); 240 * 240]; + let _ = display.fill_contiguous(&display.bounding_box(), fbuf_data); } - info!("Setup interrupts"); - let mut history = History::new(&mut sampler, &mut timer); - let mut view_state = ViewState::Main; - let mut x = 0; + let btn = Input::new( + peripherals.GPIO10, + InputConfig::default().with_pull(Pull::Down), + ); + spawner.spawn(button_listener(btn)); + spawner.spawn(event_handler()); + + loop {} +} + +#[embassy_executor::task] +async fn button_listener(mut btn: Input<'static>) { + let sender = EVENT_CHANNEL.sender(); loop { - // input state - let button_pressed = critical_section::with(|cs| { - let val = *BUTTON_PRESSED.borrow(cs).borrow(); - *BUTTON_PRESSED.borrow_ref_mut(cs) = false; - val - }); - - if button_pressed { - view_state = view_state.circulate(); - } - - if false && (history.update(&mut sampler, &mut timer) || button_pressed) { - // let iter = history.min5.oldest_ordered().map(|x| x.eco2); - // embedded_graphics::primitives::Rectangle::new(Point::zero(), fb.bounding_box().size) - // .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) - // .draw(&mut fb) - // .unwrap(); - // graph_data(iter.clone(), history.min5.len(), &mut fb); - // min_indicator(iter.clone(), history.min5.len(), &mut fb); - // max_indicator(iter.clone(), history.min5.len(), &mut fb); - // - // let img_raw = ImageRaw::::new(fb.data(), 240); - // let image = embedded_graphics::image::Image::new(&img_raw, Point::zero()); - // image.draw(&mut display).unwrap(); - - let app_state = AppState { - sample: *history.min5.last().unwrap(), - history: &history, - tendencies: Tendencies::from_history(&history), - }; - display - .bounding_box() - .into_styled(PrimitiveStyle::with_fill(BACKGROUND_COLOR)) - .draw(&mut fb) - .unwrap(); - view_state.draw_view(&mut fb, app_state); - let img_raw = ImageRaw::::new(fb.data(), 240); - let image = embedded_graphics::image::Image::new(&img_raw, Point::zero()); - image.draw(&mut display).unwrap(); - } - } - - // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0/examples/src/bin -} - -#[derive(Clone, Copy)] -pub struct Tendencies { - temperature: Tendency, - humidity: Tendency, - eco2: Tendency, - tvoc: Tendency, -} - -impl Tendencies { - pub fn from_history(history: &History) -> Self { - let mut iter = history.min5.oldest_ordered().rev().copied().take(5); - let len = history.min5.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 - }, - } + btn.wait_for_rising_edge().await; + sender.send(ApplicationEvent::ButtonPress).await; + btn.wait_for_low().await; + Timer::after_millis(20).await; } } -#[derive(Clone, Copy)] -pub struct AppState<'a> { - sample: Sample, - tendencies: Tendencies, - history: &'a History, -} - -#[derive(Clone, Copy)] -pub enum ViewState { - Main, - GraphTemperature, - GraphHumidity, - GraphECo2, - GraphTvoc, -} - -impl ViewState { - pub fn circulate(&self) -> Self { - match self { - ViewState::Main => ViewState::GraphTemperature, - ViewState::GraphTemperature => ViewState::GraphHumidity, - ViewState::GraphHumidity => ViewState::GraphECo2, - ViewState::GraphECo2 => ViewState::GraphTvoc, - ViewState::GraphTvoc => ViewState::Main, - } - } - - pub fn draw_view + GetPixel>( - &self, - target: &mut T, - app_state: AppState<'_>, - ) { - match self { - ViewState::Main => { - let _ = views::menu::menu_view(app_state) - .as_drawable(Size::new(240, 240), Rgb565::WHITE) - .draw(target); +#[embassy_executor::task] +async fn event_handler() { + let receiver = EVENT_CHANNEL.receiver(); + let mut state = false; + loop { + 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()); } - ViewState::GraphTemperature => { - detail::detailed(app_state, MenuIndicatorType::Temperature, target) - } - ViewState::GraphHumidity => { - detail::detailed(app_state, MenuIndicatorType::Humidity, target) - } - ViewState::GraphECo2 => detail::detailed(app_state, MenuIndicatorType::Co2, target), - ViewState::GraphTvoc => detail::detailed(app_state, MenuIndicatorType::Voc, target), + _ => {} } } } - -#[derive(Clone, Copy)] -pub enum MenuIndicatorType { - Temperature, - Humidity, - Co2, - Voc, -} - -impl MenuIndicatorType { - pub fn get_corresponding_icon(&self) -> &'static StaticImage { - match self { - MenuIndicatorType::Temperature => &images::TEMPERATURE_ICON, - MenuIndicatorType::Humidity => &images::HUMIDITY_ICON, - MenuIndicatorType::Co2 => &images::CO2_ICON, - MenuIndicatorType::Voc => &images::VOC_ICON, - } - } - - pub fn get_corresponding_unit_string(&self) -> &'static str { - match self { - MenuIndicatorType::Temperature => "C", - MenuIndicatorType::Humidity => "%", - MenuIndicatorType::Co2 => "ppm", - MenuIndicatorType::Voc => "ppb", - } - } - - pub fn get_value_str(&self, app_state: &AppState) -> heapless::String<16> { - match self { - MenuIndicatorType::Temperature => { - format!(16; "{:.1}", app_state.sample.temperature).unwrap() - } - MenuIndicatorType::Humidity => format!(16; "{:.1}", app_state.sample.humidity).unwrap(), - MenuIndicatorType::Co2 => format!(16; "{}", app_state.sample.eco2 as u32).unwrap(), - MenuIndicatorType::Voc => format!(16; "{}", app_state.sample.tvoc as u32).unwrap(), - } - } - - pub fn get_tendency(&self, app_state: &AppState) -> Tendency { - match self { - MenuIndicatorType::Temperature => app_state.tendencies.temperature, - MenuIndicatorType::Humidity => app_state.tendencies.humidity, - MenuIndicatorType::Co2 => app_state.tendencies.eco2, - MenuIndicatorType::Voc => app_state.tendencies.tvoc, - } - } -} - -#[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, - } - } -} - -fn tendency_indicator(tendency: Tendency) -> impl View { - HStack::new(( - Image::new(get_image!(tendency.get_corresponding_icon())) - .flex_frame() - .with_min_size(10, 20) - .with_max_size(10, 20), - Spacer::default(), - )) - .flex_frame() - .with_max_width(15) -} - -#[handler] -fn interrupt_handler() { - critical_section::with(|cs| { - let mut button = INPUT_BUTTON.borrow_ref_mut(cs); - let Some(button) = button.as_mut() else { - return; - }; - - *BUTTON_PRESSED.borrow_ref_mut(cs) = true; - button.clear_interrupt(); - }); -}