Nice graphs

This commit is contained in:
2025-11-30 22:11:47 +01:00
parent 3cb8a04a77
commit ea9852d5bc
28 changed files with 918 additions and 426 deletions

395
src/main.rs Normal file
View File

@ -0,0 +1,395 @@
#![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::default::Default;
use core::iter::Iterator;
use core::ops::Sub;
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::Level;
use esp_hal::gpio::Output;
use esp_hal::gpio::OutputConfig;
use esp_hal::gpio::Pull;
use esp_hal::interrupt;
use esp_hal::riscv::asm::delay;
use esp_hal::time::Rate;
use heapless::deque;
use heapless::format;
use heapless::history_buf;
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;
extern crate alloc;
// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
#[main]
fn main() -> ! {
// generator version: 1.0.1
images::prepare_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 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,
);
let mut display = mipidsi::Builder::new(ST7789, di)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut timer)
.unwrap();
let mut fb =
Framebuffer::<Rgb565, _, LittleEndian, 240, 240, { buffer_size::<Rgb565>(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();
let mut sampler = Sampler::new(
peripherals.I2C0,
peripherals.GPIO8,
peripherals.GPIO9,
&mut timer,
);
timer.delay_millis(2000);
let _ = sampler.sample(&mut timer);
let input_button = Input::new(
peripherals.GPIO10,
InputConfig::default().with_pull(Pull::Down),
);
let mut last_button_state = Level::Low;
let mut history = History::new(&mut sampler, &mut timer);
let mut view_state = ViewState::Main;
loop {
// input state
let mut button_pressed = false;
if last_button_state == Level::Low && input_button.is_high() {
button_pressed = true;
}
last_button_state = input_button.level();
if button_pressed {
view_state = view_state.circulate();
}
if 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::<Rgb565, LittleEndian>::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::<Rgb565, LittleEndian>::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
}
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
},
}
}
}
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<T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>>(
&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 => {
let _ = views::detail::detailed_view(
MenuIndicatorType::Temperature(10.),
Tendency::Steady,
)
.as_drawable(Size::new(240, 240), Rgb565::WHITE)
.draw(target);
let mut graph_fb = Framebuffer::<
Rgb565,
_,
LittleEndian,
240,
{ 240 - 53 },
{ buffer_size::<Rgb565>(240, 240) },
>::new();
let iter = app_state.history.min5.oldest_ordered().map(|x| x.eco2);
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 img_raw = ImageRaw::<Rgb565, LittleEndian>::new(graph_fb.data(), 240);
let image = embedded_graphics::image::Image::new(&img_raw, Point::new(0, 53));
let _ = image.draw(target);
}
_ => {
let _ = views::menu::menu_view(app_state)
.as_drawable(Size::new(240, 240), Rgb565::WHITE)
.draw(target);
}
}
}
}
pub enum MenuIndicatorType {
Temperature(f32),
Humidity(f32),
Co2(u32),
Voc(u32),
}
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) -> heapless::String<16> {
match self {
MenuIndicatorType::Temperature(temp) => format!(16; "{:.1}", temp).unwrap(),
MenuIndicatorType::Humidity(hum) => format!(16; "{:.1}", hum).unwrap(),
MenuIndicatorType::Co2(co2) => format!(16; "{}", co2).unwrap(),
MenuIndicatorType::Voc(voc) => format!(16; "{}", voc).unwrap(),
}
}
}
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<Rgb565> {
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)
}