#![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(); }); }