Initial commit

This commit is contained in:
2025-11-28 12:53:28 +01:00
commit 9a880c23ac
17 changed files with 1891 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "buoyant"]
path = buoyant
url = git@github.com:chalbin73/buoyant.git

1381
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "co2-meter-iface"
version = "0.1.0"
edition = "2024"
[dependencies]
#buoyant = "0.6.0-alpha.0"
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"
tinybmp = "0.6.0"

19
assets/Makefile Normal file
View File

@ -0,0 +1,19 @@
SVGS = $(wildcard ./*.svg)
TARGET_BMPS = $(SVGS:.svg=.bmp)
SIZE = 45
BACKCOLOR = 080808
.PHONY: all
all: $(TARGET_BMPS)
@echo $(SVGS)
clean:
rm $(TARGET_BMPS)
%.bmp: %.png
@convert $< -background "#$(BACKCOLOR)" -alpha remove -define bmp:subtype=RGB565 $@
%.png: %.svg
inkscape -h $(SIZE) -w $(SIZE) $< -o $@

BIN
assets/co2-icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

81
assets/co2-icon.svg Normal file
View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="co2-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.64"
inkscape:cx="-160.15625"
inkscape:cy="294.53125"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:36.3189px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#00ade7;fill-opacity:1;stroke-width:2.26994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
x="76.022087"
y="74.254074"
id="text1-3"
transform="scale(0.98156475,1.0187815)"
inkscape:label="2"><tspan
sodipodi:role="line"
id="tspan1-6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:36.3189px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:2.26994"
x="76.022087"
y="74.254074">2</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56.1282px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#00ade7;fill-opacity:1;stroke-width:3.50802;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
x="5.6048326"
y="81.050644"
id="text1-3-3"
transform="scale(0.98156475,1.0187815)"
inkscape:label="2"><tspan
sodipodi:role="line"
id="tspan1-6-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56.1282px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:3.50802"
x="5.6048326"
y="81.050644">C</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56.1282px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#00ade7;fill-opacity:1;stroke-width:3.50802;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
x="40.805176"
y="63.398773"
id="text1-3-3-7"
transform="scale(0.98156475,1.0187815)"
inkscape:label="2"><tspan
sodipodi:role="line"
id="tspan1-6-5-9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:56.1282px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:3.50802"
x="40.805176"
y="63.398773">O</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/humidity-icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

46
assets/humidity-icon.svg Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="38px"
height="38px"
viewBox="0 0 24 24"
stroke-width="1.6"
fill="none"
color="#bababa"
data-darkreader-inline-color=""
style="--darkreader-inline-color: var(--darkreader-text-bababa, #bcb7ae);"
version="1.1"
id="svg1"
sodipodi:docname="humidity-icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="9.5273334"
inkscape:cx="19.785179"
inkscape:cy="22.829053"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
d="M20 14C20 9.58172 12 2 12 2C12 2 4 9.58172 4 14C4 18.4183 7.58172 22 12 22C16.4183 22 20 18.4183 20 14Z"
stroke="#bababa"
stroke-width="1.6"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke:#bababa;stroke:#0137f1;stroke-opacity:1"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/temperature-icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

102
assets/temperature-icon.svg Normal file
View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24px"
height="24px"
viewBox="0 0 24 24"
stroke-width="1.5"
fill="none"
color="#ff6600"
data-darkreader-inline-color=""
style="--darkreader-inline-color: var(--darkreader-text-000000, #e8e6e3);"
version="1.1"
id="svg7"
sodipodi:docname="temperature-icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7" />
<sodipodi:namedview
id="namedview7"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="30.169889"
inkscape:cx="10.523738"
inkscape:cy="14.103466"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<path
d="M6 11.9995C4.78555 12.9117 4 14.3641 4 15.9999C4 18.7613 6.23858 20.9999 9 20.9999C11.7614 20.9999 14 18.7613 14 15.9999C14 14.3641 13.2144 12.9117 12 11.9995"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path1" />
<path
d="M6 12V3H12V12"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path2" />
<path
d="M12 3L14 3"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path3" />
<path
d="M12 6L14 6"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path4" />
<path
d="M12 9H14"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path5" />
<path
d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path6" />
<path
d="M9 14C7.89543 14 7 14.8954 7 16C7 17.1046 7.89543 18 9 18C10.1046 18 11 17.1046 11 16C11 14.8954 10.1046 14 9 14ZM9 14V11"
stroke="#ff6600"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: #ff6600;"
id="path7" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/voc-icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

37
assets/voc-icon.svg Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 24"
fill="currentColor"
version="1.1"
id="svg1"
sodipodi:docname="voc-icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="21.333333"
inkscape:cx="6.2109375"
inkscape:cy="14.226563"
inkscape:window-width="1916"
inkscape:window-height="1032"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
d="M4 4H8V6H4V4ZM16 19H20V21H16V19ZM2 9H7V11H2V9ZM9 9H12V11H9V9ZM14 9H20V11H14V9ZM4 14H10V16H4V14ZM12 14H15V16H12V14ZM17 14H22V16H17V14ZM10 4H22V6H10V4ZM2 19H14V21H2V19Z"
id="path1"
style="stroke:none;stroke-opacity:1;fill:#cacaca;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
buoyant Submodule

Submodule buoyant added at e7e8cc8d47

39
src/images.rs Normal file
View File

@ -0,0 +1,39 @@
use std::{cell::UnsafeCell, mem::MaybeUninit};
use embedded_graphics::pixelcolor::Rgb565;
use tinybmp::Bmp;
thread_local! {
pub static HUMIDITY_ICON: UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>> = const { UnsafeCell::new(MaybeUninit::zeroed()) };
pub static TEMPERATURE_ICON: UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>> = const { UnsafeCell::new(MaybeUninit::zeroed()) };
pub static VOC_ICON: UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>> = const { UnsafeCell::new(MaybeUninit::zeroed()) };
pub static CO2_ICON: UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>> = const { UnsafeCell::new(MaybeUninit::zeroed()) };
}
macro_rules! load_image {
($stat:expr, $path:expr) => {
$stat
.with(|x| *x.get() = MaybeUninit::new(Bmp::from_slice(include_bytes!($path)).unwrap()));
};
}
#[macro_export]
macro_rules! get_image {
($image:expr) => {
unsafe {
&*std::mem::transmute::<*mut MaybeUninit<Bmp<Rgb565>>, *mut Bmp<Rgb565>>(
$image.with(|g| g.get()),
)
}
};
}
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");
}
}

167
src/main.rs Normal file
View File

@ -0,0 +1,167 @@
#![feature(unsafe_cell_access)]
mod images;
mod triangle;
use core::fmt::Write;
use std::{cell::UnsafeCell, mem::MaybeUninit};
use buoyant::{primitives::Point, render::StrokedShape, view::prelude::*};
use embedded_graphics::{
pixelcolor::{Rgb565, Rgb888},
prelude::*,
};
use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay, Window};
use embedded_sprites::{include_image, sprite::Sprite};
use heapless::format;
use profont::{PROFONT_12_POINT, PROFONT_14_POINT, PROFONT_18_POINT, PROFONT_24_POINT};
use tinybmp::Bmp;
use crate::triangle::Triangle;
const BACKGROUND_COLOR: Rgb565 = Rgb565::BLACK;
const DEFAULT_COLOR: Rgb565 = Rgb565::WHITE;
fn main()
{
images::prepare_images();
let mut window = Window::new("Hello World", &OutputSettings::default());
let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
display.clear(BACKGROUND_COLOR);
hello_view()
.as_drawable(display.size(), DEFAULT_COLOR)
.draw(&mut display)
.unwrap();
window.show_static(&display);
}
fn hello_view() -> impl View<Rgb565>
{
VStack::new((
HStack::new((
main_menu_indicator(MenuIndicatorType::Temperature(31.5)),
main_menu_indicator(MenuIndicatorType::Humidity(36.2)),
))
.with_spacing(5),
HStack::new((
main_menu_indicator(MenuIndicatorType::Co2(1329)),
main_menu_indicator(MenuIndicatorType::Voc(29)),
))
.with_spacing(5),
))
.with_spacing(5)
}
const FRAME_STROKE: u32 = 2;
const FRAME_COLOR: Rgb565 = Rgb565::new(5, 9, 5);
pub enum MenuIndicatorType
{
Temperature(f32),
Humidity(f32),
Co2(u32),
Voc(u32),
}
impl MenuIndicatorType
{
pub fn get_corresponding_icon(
&self,
) -> &'static std::thread::LocalKey<UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>>>
{
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(),
}
}
}
fn main_menu_indicator(indicator_type: MenuIndicatorType) -> impl View<Rgb565>
{
Rectangle
.corner_radius(10)
.stroked(FRAME_STROKE)
.foreground_color(FRAME_COLOR)
.background(Alignment::Center, || {
ZStack::new((
Rectangle
.corner_radius(15)
.foreground_color(Rgb565::new(1, 2, 1)),
VStack::new((
HStack::new((
Spacer::default(),
buoyant::view::Image::new(get_image!(
indicator_type.get_corresponding_icon()
)),
Spacer::default(),
)),
HStack::new((
Spacer::default(),
tendency_indicator(Tendency::Rising),
Text::new(indicator_type.get_value_str(), &PROFONT_24_POINT),
Text::new(
indicator_type.get_corresponding_unit_string(),
&PROFONT_18_POINT,
)
.foreground_color(Rgb565::CSS_DARK_GRAY)
.flex_frame()
.with_infinite_max_height()
.with_vertical_alignment(VerticalAlignment::Bottom)
.with_max_height(25),
Spacer::default(),
)),
))
.with_alignment(HorizontalAlignment::Center)
.flex_frame(),
))
})
}
pub enum Tendency
{
Rising,
Steady,
Falling,
}
fn tendency_indicator(tendency: Tendency) -> impl View<Rgb565>
{
VStack::new((
StrokedShape::new(
Triangle::new(Point::new(0, 5), Point::new(10, 0), Point::new(0, 10)),
10,
),
StrokedShape::new(
Triangle::new(Point::new(0, 5), Point::new(10, 0), Point::new(0, 10)),
10,
),
))
}

0
src/triangle.rs Normal file
View File