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

View File

@ -4,10 +4,6 @@ name = "co2-meter"
rust-version = "1.88"
version = "0.1.0"
[[bin]]
name = "co2-meter"
path = "./src/bin/main.rs"
[dependencies]
esp-hal = { version = "1.0.0", features = ["defmt", "esp32c3", "unstable"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -24,7 +24,7 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="7.6922777"
inkscape:cx="29.510115"
inkscape:cx="29.575115"
inkscape:cy="28.27511"
inkscape:window-width="1916"
inkscape:window-height="1032"
@ -40,7 +40,7 @@
id="layer1"
transform="translate(0.08272917,0.15224179)">
<rect
style="fill:#242424;fill-opacity:1;stroke:none;stroke-width:1.6006;stroke-dasharray:6.40237, 3.20118, 1.6006, 3.20118;paint-order:markers stroke fill"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.6006;stroke-dasharray:6.40237, 3.20118, 1.6006, 3.20118;paint-order:markers stroke fill"
id="rect1"
width="11.90625"
height="11.90625"

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -26,8 +26,8 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="9.5273334"
inkscape:cx="19.83766"
inkscape:cy="22.724092"
inkscape:cx="19.890141"
inkscape:cy="22.829053"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
@ -35,7 +35,7 @@
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<svg:rect
style="fill:#242424;stroke-width:6.22986;stroke-dasharray:none;paint-order:markers stroke fill"
style="fill:#000000;stroke-width:6.22986;stroke-dasharray:none;paint-order:markers stroke fill;fill-opacity:1"
id="rect1"
width="28.421053"
height="28.421053"

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

@ -44,7 +44,7 @@
id="layer1"
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
<rect
style="fill:#080808;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect4"
width="98.624123"
height="197.24825"
@ -85,7 +85,7 @@
inkscape:transform-center-y="12.403106"
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
<circle
style="fill:#1c1c1c;fill-opacity:1;stroke:#080808;stroke-width:5.96404155;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
style="fill:#1c1c1c;fill-opacity:1;stroke:#000000;stroke-width:5.96404155;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path2"
cx="5.0000005"
cy="97.166092"

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

@ -44,7 +44,7 @@
id="layer1"
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
<rect
style="fill:#080808;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect4"
width="98.624123"
height="197.24825"
@ -85,7 +85,7 @@
inkscape:transform-center-y="12.403106"
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
<circle
style="fill:#1c1c1c;fill-opacity:1;stroke:#080808;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
style="fill:#1c1c1c;fill-opacity:1;stroke:#000000;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path2"
cx="5.0000005"
cy="97.166092"

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

@ -44,7 +44,7 @@
id="layer1"
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
<rect
style="fill:#080808;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect4"
width="98.624123"
height="197.24825"
@ -85,7 +85,7 @@
inkscape:transform-center-y="12.403106"
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
<circle
style="fill:#f6ab00;fill-opacity:1;stroke:#080808;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
style="fill:#f6ab00;fill-opacity:1;stroke:#000000;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path2"
cx="5.0000005"
cy="97.166092"

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -26,8 +26,8 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="10.216728"
inkscape:cx="24.861188"
inkscape:cy="27.748609"
inkscape:cx="24.910128"
inkscape:cy="27.74861"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
@ -49,7 +49,7 @@ div[style*=&quot;background-color: rgb(135, 135, 135)&quot;] {
inkscape:label="Layer 1"
transform="matrix(2.0390253,0,0,2.0390253,-3.3768322,-2.3378069)">
<svg:rect
style="fill:#242424;fill-opacity:1;stroke-width:5.46432;paint-order:markers stroke fill"
style="fill:#000000;fill-opacity:1;stroke-width:5.46432;paint-order:markers stroke fill"
id="rect1"
width="22.069368"
height="22.069368"

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -24,7 +24,7 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="12.529602"
inkscape:cx="23.624054"
inkscape:cx="23.66396"
inkscape:cy="30.687327"
inkscape:window-width="1916"
inkscape:window-height="1032"
@ -33,7 +33,7 @@
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<rect
style="fill:#242424;stroke-width:12.8688;stroke-dasharray:51.4753, 25.7377, 12.8688, 25.7377;paint-order:markers stroke fill;fill-opacity:1"
style="fill:#000000;stroke-width:12.8688;stroke-dasharray:51.4753, 25.7377, 12.8688, 25.7377;paint-order:markers stroke fill;fill-opacity:1"
id="rect1"
width="45"
height="45"

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,135 +0,0 @@
use core::iter::Iterator;
use embedded_graphics::Pixel;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::DrawTarget;
use embedded_graphics::prelude::Drawable;
use embedded_graphics::prelude::Point;
use embedded_graphics::prelude::Primitive;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::prelude::Size;
use embedded_graphics::primitives::Line;
use embedded_graphics::primitives::PrimitiveStyle;
fn map_float(x: f32, x_min: f32, x_max: f32, y_min: f32, y_max: f32) -> f32 {
((x - x_min) / (x_max - x_min)) * (y_max - y_min) + y_min
}
const DEFAULT_LUT: [Rgb565; 5] = [
Rgb565::new(0, 16, 11),
Rgb565::new(11, 20, 17),
Rgb565::new(23, 20, 18),
Rgb565::new(31, 24, 12),
Rgb565::new(31, 41, 0),
];
fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: f32) -> Rgb565 {
Rgb565::new(
(a.r() as f32 * (1. - x)) as u8 + (b.r() as f32 * x) as u8,
(a.g() as f32 * (1. - x)) as u8 + (b.g() as f32 * x) as u8,
(a.b() as f32 * (1. - x)) as u8 + (b.b() as f32 * x) as u8,
)
}
fn color_lut(mut x: f32, colors: &[Rgb565]) -> Rgb565 {
if x == 1. {
return colors[colors.len() - 1];
}
if x == 0. {
return colors[0];
}
x *= (colors.len() - 1) as f32;
let index = libm::floorf(x);
let interp = x - index;
rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp)
}
pub fn graph_data<T: DrawTarget<Color = Rgb565>>(data: &[f32], target: &mut T) {
let min = data.iter().copied().reduce(f32::min).unwrap_or(0.);
let max = data.iter().copied().reduce(f32::max).unwrap_or(0.);
data.iter().map(|x| x);
let size = Size::new(
target.bounding_box().size.width,
target.bounding_box().size.height,
);
let mut start = Point::new(
0,
map_float(data[0], min, max, size.height as f32, 0.) as i32,
);
let point_count = size.width / 2;
for y in 0..size.height {
let value = map_float(y as f32, size.height as f32, 0., min, max);
let lut_color = color_lut(
map_float(y as f32, size.height as f32, 0., 0., 1.),
&DEFAULT_LUT,
);
let color = rgb565_interpolate(Rgb565::BLACK, lut_color, 0.3);
for x in 0..size.width {
let pos = map_float(
x as f32,
0.,
size.width as f32 - 1.,
0.,
(data.len() - 1) as f32,
);
// Sample
let index = libm::floorf(pos).min((data.len() - 2) as f32);
let interpolation = pos - index;
let curve_value = data[index as usize] * (1. - interpolation)
+ data[index as usize + 1] * interpolation;
// if value <= curve_value && (x + y) % 2 == 0 {
// let _ = Pixel(Point::new(x as i32, y as i32), color).draw(target);
// }
if (x as i32 - y as i32) % 6 == 0 {
if value <= curve_value {
let mut pixel_color = Rgb565::new(1, 2, 1);
pixel_color = color;
let _ = Pixel(Point::new(x as i32 - 2, y as i32), pixel_color).draw(target);
}
}
}
}
for (i, x) in data.iter().skip(1).enumerate() {
let point = Point::new(
map_float(i as f32, 0., data.len() as f32 - 1., 0., size.width as f32) as i32,
map_float(*x, min, max, size.height as f32, 0.) as i32,
);
let factor = map_float(*x, min, max, 0., 1.);
let _ = Line::new(start, point)
.into_styled(PrimitiveStyle::with_stroke(
color_lut(factor, &DEFAULT_LUT),
2,
))
.draw(target);
start = point;
}
// for i in 0..point_count {
// // Sample data
// let index = map_float(
// i as f32,
// 0.,
// point_count as f32,
// 0 as f32,
// data.len() as f32,
// ) as usize;
// let value = data[index];
// let point = Point::new(
// map_float(i as f32, 0., point_count as f32, 0., size.width as f32) as i32,
// map_float(value, min, max, 0., size.height as f32) as i32,
// );
//
// let _ = Line::new(start, point)
// .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 2))
// .draw(target);
// start = point;
// }
}

View File

@ -1,199 +0,0 @@
#![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::Size;
use buoyant::view::AsDrawable;
use buoyant::view::Image;
use buoyant::view::ViewExt;
use core::default::Default;
use core::iter::Iterator;
use defmt::info;
use embedded_graphics::Drawable;
use embedded_graphics::framebuffer::Framebuffer;
use embedded_graphics::framebuffer::buffer_size;
use embedded_graphics::image::ImageRaw;
use embedded_graphics::pixelcolor::raw::LittleEndian;
use embedded_graphics::prelude::Point;
use embedded_graphics::prelude::RgbColor;
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::gpio::Level;
use esp_hal::gpio::Output;
use esp_hal::gpio::OutputConfig;
use esp_hal::time::Rate;
use heapless::format;
use mipidsi::interface::SpiInterface;
use mipidsi::models::ST7789;
use mipidsi::options::Orientation;
use mipidsi::options::Rotation;
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 tinybmp::Bmp;
use crate::graph::graph_data;
use crate::images::StaticImage;
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.GPIO1, Level::High, OutputConfig::default());
cs_output.set_high();
let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap();
let _ = Output::new(peripherals.GPIO0, Level::High, OutputConfig::default());
let mut buffer = [0_u8; 512];
// Define the display interface with no chip select
let di = SpiInterface::new(
spi_device,
Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default()),
&mut buffer,
);
let mut display = mipidsi::Builder::new(ST7789, di)
.reset_pin(Output::new(
peripherals.GPIO3,
Level::Low,
OutputConfig::default(),
))
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut timer)
.unwrap();
let mut fb =
Framebuffer::<Rgb565, _, LittleEndian, 240, 240, { buffer_size::<Rgb565>(240, 240) }>::new(
);
//fb.clear(Rgb565::BLACK).unwrap();
// views::menu::menu_view()
// .as_drawable(Size::new(240, 240), Rgb565::WHITE)
// .draw(&mut fb)
// .unwrap();
let mut x = [0.; 100];
for (i, x) in x.iter_mut().enumerate() {
*x = libm::sinf(i as f32 / 10.);
}
graph_data(&x, &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();
// embedded_graphics::image::Image::new(get_image!(images::HUMIDITY_ICON), Point::zero())
// .draw(&mut display)
// .unwrap();
info!("Finished !");
loop {
core::hint::spin_loop();
}
// 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 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)
}

View File

@ -1,53 +0,0 @@
use core::cell::RefCell;
use aht20_driver::AHT20;
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::default::Default;
use embedded_hal_bus::i2c::RcDevice;
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;
pub struct Sampler<'a> {
ens160: Ens160<RcDevice<I2c<'a, Blocking>>>,
aht20: aht20_driver::AHT20Initialized<RcDevice<I2c<'a, Blocking>>>,
}
impl<'a> Sampler<'a> {
pub fn new(
i2c: impl Instance + 'a,
sda: impl PeripheralOutput<'a>,
scl: impl PeripheralOutput<'a>,
mut timer: Delay,
) -> Self {
let i2c = I2c::new(i2c, Default::default())
.unwrap()
.with_sda(sda)
.with_scl(scl);
let i2c = Rc::new(RefCell::new(i2c));
let mut ens160 = Ens160::new(embedded_hal_bus::i2c::RcDevice::new(i2c.clone()), 0x53);
timer.delay_millis(500);
ens160.reset().unwrap();
timer.delay_millis(500);
ens160.operational().unwrap();
let aht20_uninit = AHT20::new(
embedded_hal_bus::i2c::RcDevice::new(i2c.clone()),
aht20_driver::SENSOR_ADDRESS,
);
let aht20 = aht20_uninit.init(&mut timer).unwrap();
Sampler { ens160, aht20 }
}
}

View File

@ -10,4 +10,4 @@ pub const FRAME_STROKE_COLOR: Rgb565 = Rgb565::new(4, 9, 4);
pub const MAIN_TEXT_COLOR: Rgb565 = Rgb565::WHITE;
pub const SUB_TEXT_COLOR: Rgb565 = Rgb565::CSS_DARK_GRAY;
pub const FRAME_STROKE: u32 = 3;
pub const FRAME_STROKE: u32 = 1;

264
src/graph.rs Normal file
View File

@ -0,0 +1,264 @@
use embedded_graphics::Pixel;
use embedded_graphics::image::GetPixel;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::DrawTarget;
use embedded_graphics::prelude::Drawable;
use embedded_graphics::prelude::Point;
use embedded_graphics::prelude::Primitive;
use embedded_graphics::prelude::RgbColor;
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;
fn map_float(x: f32, x_min: f32, x_max: f32, y_min: f32, y_max: f32) -> f32 {
((x - x_min) / (x_max - x_min)) * (y_max - y_min) + y_min
}
// const DEFAULT_LUT: [Rgb565; 5] = [
// Rgb565::new(0, 16, 11),
// Rgb565::new(11, 20, 17),
// Rgb565::new(23, 20, 18),
// Rgb565::new(31, 24, 12),
// Rgb565::new(31, 41, 0),
// ];
const DEFAULT_LUT: [Rgb565; 4] = [Rgb565::GREEN, Rgb565::YELLOW, Rgb565::RED, Rgb565::RED];
fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: f32) -> Rgb565 {
Rgb565::new(
(a.r() as f32 * (1. - x)) as u8 + (b.r() as f32 * x) as u8,
(a.g() as f32 * (1. - x)) as u8 + (b.g() as f32 * x) as u8,
(a.b() as f32 * (1. - x)) as u8 + (b.b() as f32 * x) as u8,
)
}
fn color_lut(mut x: f32, colors: &[Rgb565]) -> Rgb565 {
if x == 1. {
return colors[colors.len() - 1];
}
if x == 0. {
return colors[0];
}
x *= (colors.len() - 1) as f32;
let index = libm::floorf(x);
let interp = x - index;
rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp)
}
pub fn graph_data<I, T>(data: I, data_count: usize, target: &mut T)
where
I: Iterator<Item = f32> + Clone,
T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>,
{
let min = data.clone().reduce(f32::min).unwrap_or(0.);
let max = data.clone().reduce(f32::max).unwrap_or(0.);
let size = Size::new(
target.bounding_box().size.width,
target.bounding_box().size.height,
);
// Draw data as WHITE line
let mut start = Point::new(
0,
map_float(
data.clone().next().unwrap(),
min,
max,
size.height as f32,
0.,
) as i32,
);
for (i, x) in data.clone().skip(1).enumerate() {
let point = Point::new(
map_float(i as f32, 0., data_count as f32 - 2., 0., size.width as f32) as i32,
map_float(x, min, max, size.height as f32, 0.) as i32,
);
let _ = Line::new(start, point)
.into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 2))
.draw(target);
start = point;
}
for x in 0..size.width {
// Start coloring from up to bottom
let mut met_curve = false;
for y in 0..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 {
let _ = Pixel(position, height_color).draw(target);
met_curve = true;
} else if met_curve && (x as i32 - y as i32) % 7 == 0 {
let _ = Pixel(
position,
rgb565_interpolate(height_color, Rgb565::BLACK, 1. - height_factor),
)
.draw(target);
}
}
}
}
pub fn min_indicator<
I: Iterator<Item = f32> + Clone,
T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>,
>(
data: I,
data_count: usize,
target: &mut T,
) {
let size = target.bounding_box().size;
let (min_index, min) = data
.clone()
.enumerate()
.reduce(|a, b| if a.1 < b.1 { a } else { b })
.unwrap_or((0, 0.));
let min_x = map_float(
min_index as f32,
0.,
data_count as f32,
0.,
size.width as f32,
) as i32;
// let _ = Line::new(Point::new(min_x, 0), Point::new(min_x, size.height as i32))
// .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1))
// .draw(target);
for y in 0..size.height {
if (y / 2) % 2 == 0 {
let position = Point::new(min_x, y as i32);
let _ = Pixel(
position,
rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6),
)
.draw(target);
// let position = Point::new(min_x + 1, y as i32);
// let _ = Pixel(
// position,
// rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6),
// )
// .draw(target);
}
}
let minimum_text = "Minimum";
let font = &PROFONT_10_POINT;
let text_start = if min_x < (size.width / 2) as i32 {
5
} else {
-((minimum_text.len() + 1) as i32 * font.character_size.width as i32 + 3)
};
let style = MonoTextStyle::new(
&PROFONT_10_POINT,
rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8),
);
let _ = Text::new(
minimum_text,
Point::new(min_x + text_start as i32, 10),
style,
)
.draw(target);
let value = format!(16; "{:.1}", min).unwrap();
let _ = Text::new(
value.as_str(),
Point::new(
min_x + text_start as i32,
10 + font.character_size.height as i32,
),
style,
)
.draw(target);
}
pub fn max_indicator<
T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>,
I: Iterator<Item = f32> + Clone,
>(
data: I,
data_count: usize,
target: &mut T,
) {
let size = target.bounding_box().size;
let (max_index, max) = data
.clone()
.enumerate()
.reduce(|a, b| if a.1 > b.1 { a } else { b })
.unwrap_or((0, 0.));
let max_x = map_float(
max_index as f32,
0.,
data_count as f32,
0.,
size.width as f32,
) as i32;
for y in 0..size.height {
if (y / 2) % 2 == 0 {
let position = Point::new(max_x, y as i32);
let _ = Pixel(
position,
rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6),
)
.draw(target);
// let position = Point::new(max_x + 1, y as i32);
// let _ = Pixel(
// position,
// rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6),
// )
// .draw(target);
}
}
let maximum_text = "Maximum";
let font = &PROFONT_10_POINT;
let text_start = if max_x < (size.width / 2) as i32 {
5
} else {
-((maximum_text.len() + 1) as i32 * font.character_size.width as i32 + 3)
};
let style = MonoTextStyle::new(
&PROFONT_10_POINT,
rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8),
);
let _ = Text::new(
maximum_text,
Point::new(
max_x + text_start as i32,
size.height as i32 - font.character_size.height as i32 * 2,
),
style,
)
.draw(target);
let value = format!(16; "{:.1}", max).unwrap();
let _ = Text::new(
value.as_str(),
Point::new(
max_x + text_start as i32,
size.height as i32 - font.character_size.height as i32,
),
style,
)
.draw(target);
}

View File

@ -43,13 +43,13 @@ macro_rules! get_image {
pub fn prepare_images() {
unsafe {
load_image!(HUMIDITY_ICON, "../../assets/humidity-icon.bmp");
load_image!(TEMPERATURE_ICON, "../../assets/temperature-icon.bmp");
load_image!(VOC_ICON, "../../assets/voc-icon.bmp");
load_image!(CO2_ICON, "../../assets/co2-icon.bmp");
load_image!(HUMIDITY_ICON, "../assets/humidity-icon.bmp");
load_image!(TEMPERATURE_ICON, "../assets/temperature-icon.bmp");
load_image!(VOC_ICON, "../assets/voc-icon.bmp");
load_image!(CO2_ICON, "../assets/co2-icon.bmp");
load_image!(TENDENCY_RISING, "../../assets/indic-rising.bmp");
load_image!(TENDENCY_STEADY, "../../assets/indic-steady.bmp");
load_image!(TENDENCY_FALLING, "../../assets/indic-falling.bmp");
load_image!(TENDENCY_RISING, "../assets/indic-rising.bmp");
load_image!(TENDENCY_STEADY, "../assets/indic-steady.bmp");
load_image!(TENDENCY_FALLING, "../assets/indic-falling.bmp");
}
}

View File

@ -1 +0,0 @@
#![no_std]

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)
}

209
src/sampler.rs Normal file
View File

@ -0,0 +1,209 @@
use core::cell::RefCell;
use core::ops::Add;
use core::ops::Mul;
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::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<RcDevice<I2c<'a, Blocking>>>,
aht20: aht20_driver::AHT20Initialized<RcDevice<I2c<'a, Blocking>>>,
}
#[derive(Clone, Copy, Debug)]
pub struct Sample {
pub temperature: f32,
pub humidity: f32,
pub eco2: f32,
pub tvoc: f32,
}
impl Sample {
pub fn zero() -> Self {
Sample {
temperature: 0.,
humidity: 0.,
eco2: 0.,
tvoc: 0.,
}
}
}
impl Add<Sample> for Sample {
type Output = Sample;
fn add(self, rhs: Sample) -> Self::Output {
Sample {
temperature: self.temperature + rhs.temperature,
humidity: self.humidity + rhs.humidity,
eco2: self.eco2 + rhs.eco2,
tvoc: self.tvoc + rhs.tvoc,
}
}
}
impl Sub<Sample> for Sample {
type Output = Sample;
fn sub(self, rhs: Sample) -> Self::Output {
Sample {
temperature: self.temperature - rhs.temperature,
humidity: self.humidity - rhs.humidity,
eco2: self.eco2 - rhs.eco2,
tvoc: self.tvoc - rhs.tvoc,
}
}
}
impl Mul<Sample> for Sample {
type Output = Sample;
fn mul(self, rhs: Sample) -> Self::Output {
Sample {
temperature: self.temperature * rhs.temperature,
humidity: self.humidity * rhs.humidity,
eco2: self.eco2 * rhs.eco2,
tvoc: self.tvoc * rhs.tvoc,
}
}
}
impl Mul<f32> for Sample {
type Output = Sample;
fn mul(self, rhs: f32) -> Self::Output {
Sample {
temperature: self.temperature * rhs,
humidity: self.humidity * rhs,
eco2: self.eco2 * rhs,
tvoc: self.tvoc * rhs,
}
}
}
impl<'a> Sampler<'a> {
pub fn new(
i2c: impl Instance + 'a,
sda: impl PeripheralOutput<'a>,
scl: impl PeripheralOutput<'a>,
timer: &mut Delay,
) -> Self {
let i2c = I2c::new(i2c, Default::default())
.unwrap()
.with_sda(sda)
.with_scl(scl);
let i2c = Rc::new(RefCell::new(i2c));
let mut ens160 = Ens160::new(embedded_hal_bus::i2c::RcDevice::new(i2c.clone()), 0x53);
timer.delay_millis(500);
ens160.reset().unwrap();
timer.delay_millis(500);
ens160.operational().unwrap();
let aht20_uninit = AHT20::new(
embedded_hal_bus::i2c::RcDevice::new(i2c.clone()),
aht20_driver::SENSOR_ADDRESS,
);
let aht20 = aht20_uninit.init(timer).unwrap();
Sampler { ens160, aht20 }
}
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,
eco2: *self.ens160.eco2().unwrap() as f32,
tvoc: self.ens160.tvoc().unwrap() as f32,
}
}
}
pub const SECONDS_PER_SAMPLES: usize = 1;
pub const MIN_5_LENGTH: usize = (5 * 60) / SECONDS_PER_SAMPLES;
pub struct History {
// 5 minutes, every 5 seconds
pub min5: heapless::history_buf::HistoryBuf<Sample, MIN_5_LENGTH>,
// 2 hours every 5 seconds
pub hour2: heapless::history_buf::HistoryBuf<Sample, { (2 * 60) / SECONDS_PER_SAMPLES }>,
// 24 hours every 5 minutes
pub day: heapless::history_buf::HistoryBuf<Sample, { (24 * 60) / 5 }>,
samples_since_day: u32,
last_sample: Instant,
}
impl History {
pub fn new(sampler: &mut Sampler, timer: &mut Delay) -> Self {
let mut min5 = HistoryBuf::new();
let mut hour2 = HistoryBuf::new();
let mut day = HistoryBuf::new();
// First sampler
let sample = sampler.sample(timer);
min5.write(sample);
hour2.write(sample);
day.write(sample);
History {
min5,
hour2,
day,
samples_since_day: 0,
last_sample: Instant::now(),
}
}
pub fn update(&mut self, sampler: &mut Sampler, timer: &mut Delay) -> bool {
let now = Instant::now();
if now - self.last_sample > Duration::from_secs(SECONDS_PER_SAMPLES as u64) {
let sample = sampler.sample(timer);
self.last_sample = Instant::now();
self.samples_since_day += 1;
if self.samples_since_day as usize == MIN_5_LENGTH {
// Compute average
let avg = self.min5.iter().fold(Sample::zero(), |a, b| a + *b)
* (1. / self.min5.len() as f32);
self.day.write(avg);
self.samples_since_day = 0;
}
self.min5.write(sample);
self.hour2.write(sample);
true
} else {
false
}
}
}

View File

@ -4,12 +4,15 @@ use buoyant::view::ZStack;
use buoyant::view::shape::Rectangle;
use embedded_graphics::pixelcolor::Rgb565;
use crate::colors::BACKGROUND_COLOR;
use crate::get_image;
use crate::images::StaticImage;
pub fn icon_box_view(box_color: Rgb565, icon: &'static StaticImage) -> impl View<Rgb565> {
ZStack::new((
Rectangle.corner_radius(10).foreground_color(box_color),
Rectangle
.corner_radius(10)
.foreground_color(BACKGROUND_COLOR),
buoyant::view::Image::new(get_image!(icon)),
))
.flex_frame()

View File

@ -7,6 +7,7 @@ use embedded_graphics::prelude::*;
use profont::PROFONT_18_POINT;
use profont::PROFONT_24_POINT;
use crate::AppState;
use crate::MenuIndicatorType;
use crate::Tendency;
use crate::colors::FRAME_BACKGROUD_COLOR;
@ -17,25 +18,37 @@ use crate::colors::SUB_TEXT_COLOR;
use crate::get_image;
use crate::views::icon::icon_box_view;
pub fn menu_view() -> impl View<Rgb565> {
pub fn menu_view(app_state: AppState) -> impl View<Rgb565> {
VStack::new((
HStack::new((
main_menu_indicator(MenuIndicatorType::Temperature(31.5), Tendency::Falling),
main_menu_indicator(MenuIndicatorType::Humidity(36.2), Tendency::Steady),
main_menu_indicator(
MenuIndicatorType::Temperature(app_state.sample.temperature),
app_state.tendencies.temperature,
),
main_menu_indicator(
MenuIndicatorType::Humidity(app_state.sample.humidity),
app_state.tendencies.humidity,
),
))
.with_spacing(5),
.with_spacing(2),
HStack::new((
main_menu_indicator(MenuIndicatorType::Co2(1329), Tendency::Rising),
main_menu_indicator(MenuIndicatorType::Voc(29), Tendency::Falling),
main_menu_indicator(
MenuIndicatorType::Co2(app_state.sample.eco2 as u32),
app_state.tendencies.eco2,
),
main_menu_indicator(
MenuIndicatorType::Voc(app_state.sample.tvoc as u32),
app_state.tendencies.tvoc,
),
))
.with_spacing(5),
.with_spacing(2),
))
.with_spacing(5)
.with_spacing(2)
}
fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) -> impl View<Rgb565> {
Rectangle
.corner_radius(10)
.corner_radius(5)
.stroked(FRAME_STROKE)
.foreground_color(FRAME_STROKE_COLOR)
.background(Alignment::Center, || {