#![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." )] 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::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::Pull; 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 display; mod graph; mod images; mod sampler; 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, LongButtonPress, NewSample(Sample), } 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) { esp_alloc::heap_allocator!(size: 32 * 1024); // generator version: 1.0.1 info!("Starting up."); images::prepare_images(); info!("Prepared images."); let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); 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."); info!("Setting up display"); let mut timer = esp_hal::delay::Delay::new(); let mut display = display::setup_display( peripherals.SPI2, peripherals.GPIO4, peripherals.GPIO6, peripherals.GPIO0, peripherals.GPIO1, &mut timer, ); info!("Clearing screen"); { 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), ); // Setup sampler let sampler = SAMPLER.init(Sampler::new( peripherals.I2C0, peripherals.GPIO8, peripherals.GPIO9, &mut timer, )); 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; sender.send(ApplicationEvent::ButtonPress).await; btn.wait_for_low().await; Timer::after_millis(20).await; } } #[embassy_executor::task] async fn event_handler() { info!("Event handler task launched"); let receiver = EVENT_CHANNEL.receiver(); 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 => { 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, } } }