diff --git a/Cargo.lock b/Cargo.lock
index c2864ef..a738340 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -229,6 +229,7 @@ dependencies = [
"embedded-sprites",
"heapless 0.9.2",
"profont",
+ "rand",
"tinybmp",
]
diff --git a/Cargo.toml b/Cargo.toml
index 59e9e4f..7b50a33 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,12 +4,11 @@ version = "0.1.0"
edition = "2024"
[dependencies]
-#buoyant = "0.6.0-alpha.0"
buoyant = "0.5.3"
-#buoyant = {path = "buoyant" }
embedded-graphics = "0.8.1"
embedded-graphics-simulator = "0.8.0"
embedded-sprites = "0.2.0"
heapless = "0.9.2"
profont = "0.7.0"
+rand = "0.9.2"
tinybmp = "0.6.0"
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 = x.floor();
+ let interp = x - index;
+
+ rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp)
+}
+
+pub fn graph_data + GetPixel>(
+ 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.);
+ 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[0], min, max, size.height as f32, 0.) as i32,
+ );
+ 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 _ = 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 + GetPixel>(
+ data: &[f32],
+ target: &mut T,
+) {
+ let size = target.bounding_box().size;
+ let (min_index, min) = data
+ .iter()
+ .copied()
+ .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.len() 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 + GetPixel>(
+ data: &[f32],
+ target: &mut T,
+) {
+ let size = target.bounding_box().size;
+ let (max_index, max) = data
+ .iter()
+ .copied()
+ .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.len() 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/main.rs b/src/main.rs
index 6833498..7a5fb47 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
//#![feature(unsafe_cell_access)]
mod colors;
+mod graph;
mod images;
mod views;
@@ -8,7 +9,11 @@ use std::cell::UnsafeCell;
use std::mem::MaybeUninit;
use buoyant::view::prelude::*;
+use embedded_graphics::framebuffer::Framebuffer;
+use embedded_graphics::framebuffer::buffer_size;
+use embedded_graphics::image::ImageRaw;
use embedded_graphics::pixelcolor::Rgb565;
+use embedded_graphics::pixelcolor::raw::LittleEndian;
use embedded_graphics::prelude::*;
use embedded_graphics_simulator::OutputSettings;
use embedded_graphics_simulator::SimulatorDisplay;
@@ -16,6 +21,10 @@ use embedded_graphics_simulator::Window;
use heapless::format;
use tinybmp::Bmp;
+use crate::graph::graph_data;
+use crate::graph::max_indicator;
+use crate::graph::min_indicator;
+
const BACKGROUND_COLOR: Rgb565 = Rgb565::BLACK;
const DEFAULT_COLOR: Rgb565 = Rgb565::WHITE;
@@ -23,22 +32,50 @@ fn main() {
images::prepare_images();
let mut window = Window::new("Hello World", &OutputSettings::default());
- let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(320, 240));
+ let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(240, 240));
display.clear(BACKGROUND_COLOR);
- // views::menu::menu_view()
- // .as_drawable(display.size(), DEFAULT_COLOR)
- // .draw(&mut display)
- // .unwrap();
-
- views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady)
+ views::menu::menu_view()
.as_drawable(display.size(), DEFAULT_COLOR)
.draw(&mut display)
.unwrap();
+ // views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady)
+ // .as_drawable(display.size(), DEFAULT_COLOR)
+ // .draw(&mut display)
+ // .unwrap();
+
+ let mut dummy_data = [0.; 100];
+ for i in 0..dummy_data.len() {
+ dummy_data[i] = rand::random::();
+ }
+
+ let smoothed = dummy_data
+ .windows(30)
+ .map(|x| x.iter().sum::() / 10.)
+ .collect::>();
+
+ let mut fb = Framebuffer::<
+ Rgb565,
+ _,
+ LittleEndian,
+ 240,
+ { 240 - 80 },
+ { buffer_size::(240, 240) },
+ >::new();
+
+ // graph_data(&smoothed, &mut fb);
+ // min_indicator(&smoothed, &mut fb);
+ //max_indicator(&smoothed, &mut fb);
+
+ let img_raw = ImageRaw::::new(fb.data(), 240);
+ let image = embedded_graphics::image::Image::new(&img_raw, Point::new(0, 60));
+ //image.draw(&mut display).unwrap();
+
window.show_static(&display);
}
+#[derive(Clone, Copy)]
pub enum MenuIndicatorType {
Temperature(f32),
Humidity(f32),
@@ -77,6 +114,7 @@ impl MenuIndicatorType {
}
}
+#[derive(Clone, Copy)]
pub enum Tendency {
Rising,
Steady,
diff --git a/src/views/detail.rs b/src/views/detail.rs
index 0bcf0ff..69fdbaf 100644
--- a/src/views/detail.rs
+++ b/src/views/detail.rs
@@ -6,12 +6,18 @@ use buoyant::view::Text;
use buoyant::view::VStack;
use buoyant::view::View;
use buoyant::view::ViewExt;
+use buoyant::view::ZStack;
+use buoyant::view::shape::Rectangle;
+use buoyant::view::shape::ShapeExt;
use embedded_graphics::pixelcolor::Rgb565;
use profont::PROFONT_18_POINT;
use profont::PROFONT_24_POINT;
use crate::MenuIndicatorType;
use crate::Tendency;
+use crate::colors::BACKGROUND_COLOR;
+use crate::colors::FRAME_BACKGROUD_COLOR;
+use crate::colors::FRAME_STROKE;
use crate::colors::FRAME_STROKE_COLOR;
use crate::colors::MAIN_TEXT_COLOR;
use crate::colors::SUB_TEXT_COLOR;
@@ -20,33 +26,36 @@ use crate::views::icon::icon_box_view;
pub fn detailed_view(indicator: MenuIndicatorType, tendency: Tendency) -> impl View {
VStack::new((
- // Header
- HStack::new((
- icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()),
- Spacer::default().flex_frame().with_max_width(10),
- tendency_indicator(tendency),
- Text::new(indicator.get_value_str(), &PROFONT_24_POINT)
- .foreground_color(MAIN_TEXT_COLOR),
- Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT)
- .foreground_color(SUB_TEXT_COLOR)
+ ZStack::new((
+ header(indicator, tendency),
+ Rectangle
+ .corner_radius(10)
+ .stroked(FRAME_STROKE)
+ .foreground_color(BACKGROUND_COLOR)
.flex_frame()
- .with_infinite_max_height()
- .with_vertical_alignment(VerticalAlignment::Bottom)
- .with_max_height(25),
- Spacer::default(),
- Text::new("Temperature", &PROFONT_18_POINT)
- .foreground_color(SUB_TEXT_COLOR)
- .flex_frame()
- .with_infinite_max_height()
- .with_vertical_alignment(VerticalAlignment::Bottom)
- .with_max_height(25),
- Spacer::default().flex_frame().with_max_width(10),
+ .with_max_height(53),
)),
// Window
- Spacer::default()
- .flex_frame()
- .with_infinite_max_width()
- .with_infinite_max_height(),
+ Spacer::default(),
))
+ .with_spacing(2)
.with_alignment(HorizontalAlignment::Leading)
}
+
+pub fn header(indicator: MenuIndicatorType, tendency: Tendency) -> impl View {
+ // Header
+ HStack::new((
+ icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()),
+ Spacer::default().flex_frame().with_max_width(10),
+ Spacer::default(),
+ tendency_indicator(tendency),
+ Text::new(indicator.get_value_str(), &PROFONT_24_POINT).foreground_color(MAIN_TEXT_COLOR),
+ Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT)
+ .foreground_color(SUB_TEXT_COLOR)
+ .flex_frame()
+ .with_infinite_max_height()
+ .with_vertical_alignment(VerticalAlignment::Bottom)
+ .with_max_height(25),
+ Spacer::default(),
+ ))
+}
diff --git a/src/views/icon.rs b/src/views/icon.rs
index 8d0f5c1..6fc616b 100644
--- a/src/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::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/views/menu.rs b/src/views/menu.rs
index 23d7a7a..c30469a 100644
--- a/src/views/menu.rs
+++ b/src/views/menu.rs
@@ -23,19 +23,19 @@ pub fn menu_view() -> impl View {
main_menu_indicator(MenuIndicatorType::Temperature(31.5), Tendency::Falling),
main_menu_indicator(MenuIndicatorType::Humidity(36.2), Tendency::Steady),
))
- .with_spacing(5),
+ .with_spacing(2),
HStack::new((
main_menu_indicator(MenuIndicatorType::Co2(1329), Tendency::Rising),
main_menu_indicator(MenuIndicatorType::Voc(29), Tendency::Falling),
))
- .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, || {