diff --git a/Cargo.toml b/Cargo.toml
index 54b5827..bd1bf02 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/assets/co2-icon.bmp b/assets/co2-icon.bmp
index a74e213..e69df2d 100644
Binary files a/assets/co2-icon.bmp and b/assets/co2-icon.bmp differ
diff --git a/assets/co2-icon.svg b/assets/co2-icon.svg
index 391b8ec..ed04641 100644
--- a/assets/co2-icon.svg
+++ b/assets/co2-icon.svg
@@ -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)">
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>(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;
- // }
-}
diff --git a/src/bin/main.rs b/src/bin/main.rs
deleted file mode 100644
index d756fc2..0000000
--- a/src/bin/main.rs
+++ /dev/null
@@ -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:
-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::(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::::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 {
- 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)
-}
diff --git a/src/bin/sampler.rs b/src/bin/sampler.rs
deleted file mode 100644
index f4426bb..0000000
--- a/src/bin/sampler.rs
+++ /dev/null
@@ -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>>,
- aht20: aht20_driver::AHT20Initialized>>,
-}
-
-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 }
- }
-}
diff --git a/src/bin/colors.rs b/src/colors.rs
similarity index 93%
rename from src/bin/colors.rs
rename to src/colors.rs
index f37e25d..3fbe474 100644
--- a/src/bin/colors.rs
+++ b/src/colors.rs
@@ -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;
diff --git a/src/graph.rs b/src/graph.rs
new file mode 100644
index 0000000..56f18d1
--- /dev/null
+++ b/src/graph.rs
@@ -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(data: I, data_count: usize, target: &mut T)
+where
+ I: Iterator- + Clone,
+ T: DrawTarget + GetPixel,
+{
+ 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
- + Clone,
+ T: DrawTarget + GetPixel,
+>(
+ 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 + GetPixel,
+ I: Iterator
- + 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);
+}
diff --git a/src/bin/images.rs b/src/images.rs
similarity index 76%
rename from src/bin/images.rs
rename to src/images.rs
index 4e0785e..3c3da9c 100644
--- a/src/bin/images.rs
+++ b/src/images.rs
@@ -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");
}
}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 0c9ac1a..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1 +0,0 @@
-#![no_std]
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..269dc2f
--- /dev/null
+++ b/src/main.rs
@@ -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:
+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::(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::::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
+}
+
+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 + 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 => {
+ 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::(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::::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 {
+ 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)
+}
diff --git a/src/sampler.rs b/src/sampler.rs
new file mode 100644
index 0000000..ffe7619
--- /dev/null
+++ b/src/sampler.rs
@@ -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>>,
+ aht20: aht20_driver::AHT20Initialized>>,
+}
+
+#[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 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 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 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 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,
+
+ // 2 hours every 5 seconds
+ pub hour2: heapless::history_buf::HistoryBuf,
+
+ // 24 hours every 5 minutes
+ pub day: heapless::history_buf::HistoryBuf,
+
+ 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
+ }
+ }
+}
diff --git a/src/bin/views.rs b/src/views.rs
similarity index 100%
rename from src/bin/views.rs
rename to src/views.rs
diff --git a/src/bin/views/detail.rs b/src/views/detail.rs
similarity index 100%
rename from src/bin/views/detail.rs
rename to src/views/detail.rs
diff --git a/src/bin/views/icon.rs b/src/views/icon.rs
similarity index 77%
rename from src/bin/views/icon.rs
rename to src/views/icon.rs
index 8d0f5c1..9fbaf0a 100644
--- a/src/bin/views/icon.rs
+++ b/src/views/icon.rs
@@ -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 {
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()
diff --git a/src/bin/views/menu.rs b/src/views/menu.rs
similarity index 75%
rename from src/bin/views/menu.rs
rename to src/views/menu.rs
index 23d7a7a..080944d 100644
--- a/src/bin/views/menu.rs
+++ b/src/views/menu.rs
@@ -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 {
+pub fn menu_view(app_state: AppState) -> impl View {
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 {
Rectangle
- .corner_radius(10)
+ .corner_radius(5)
.stroked(FRAME_STROKE)
.foreground_color(FRAME_STROKE_COLOR)
.background(Alignment::Center, || {