Compare commits

...

4 Commits

Author SHA1 Message Date
43fead021c Fast graphs 2025-12-04 13:27:55 +01:00
56f9c423e7 Faster fill 2025-12-02 23:08:29 +01:00
9925499eff Embassy 2025-12-02 20:43:50 +01:00
71c1dc4fd9 Interrupts 2025-12-02 10:09:43 +01:00
9 changed files with 1612 additions and 508 deletions

422
Cargo.lock generated
View File

@ -2,6 +2,17 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aht20-async"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c93801d1c9009ecef32e156413ef8f318478d1e7c7e38dc96654f412cf37827"
dependencies = [
"bitflags 2.10.0",
"crc_all",
"embedded-hal-async",
]
[[package]]
name = "aht20-driver"
version = "2.0.0"
@ -16,6 +27,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d"
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
version = "1.5.0"
@ -107,22 +124,32 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
name = "co2-meter"
version = "0.1.0"
dependencies = [
"aht20-async",
"aht20-driver",
"buoyant",
"critical-section",
"defmt 1.0.1",
"embassy-executor",
"embassy-sync 0.7.2",
"embassy-time",
"embedded-graphics",
"embedded-graphics-framebuf",
"embedded-hal-bus",
"ens160",
"ens160-aq",
"esp-alloc",
"esp-backtrace",
"esp-bootloader-esp-idf",
"esp-hal",
"esp-println",
"esp-rtos",
"fixed",
"fixed-macro",
"heapless 0.9.2",
"libm",
"mipidsi",
"profont",
"static_cell",
"tinybmp",
]
@ -138,12 +165,24 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73"
[[package]]
name = "crc_all"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c46c1a17ebeef917714db3ae9a17bd2184f7e9977d8e020c6c8bcf59a28a6f1b"
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@ -261,7 +300,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
"thiserror 2.0.17",
]
[[package]]
@ -310,6 +349,36 @@ dependencies = [
"nb 1.1.0",
]
[[package]]
name = "embassy-executor"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b"
dependencies = [
"critical-section",
"document-features",
"embassy-executor-macros",
"embassy-executor-timer-queue",
]
[[package]]
name = "embassy-executor-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472"
dependencies = [
"darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "embassy-executor-timer-queue"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c"
[[package]]
name = "embassy-futures"
version = "0.1.2"
@ -357,6 +426,41 @@ dependencies = [
"heapless 0.8.0",
]
[[package]]
name = "embassy-time"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65"
dependencies = [
"cfg-if",
"critical-section",
"document-features",
"embassy-time-driver",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
"embedded-hal-async",
"futures-core",
]
[[package]]
name = "embassy-time-driver"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6"
dependencies = [
"document-features",
]
[[package]]
name = "embassy-time-queue-utils"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454"
dependencies = [
"embassy-executor-timer-queue",
"heapless 0.8.0",
]
[[package]]
name = "embedded-can"
version = "0.4.1"
@ -366,6 +470,15 @@ dependencies = [
"nb 1.1.0",
]
[[package]]
name = "embedded-dma"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-graphics"
version = "0.8.1"
@ -389,6 +502,16 @@ dependencies = [
"byteorder",
]
[[package]]
name = "embedded-graphics-framebuf"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22354420f68727fa24d1e2741dae1e9a041065e80fb63b35a8d19c647a85be76"
dependencies = [
"embedded-dma",
"embedded-graphics",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
@ -477,6 +600,20 @@ dependencies = [
"embedded-storage",
]
[[package]]
name = "embuild"
version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4caa4f198bb9152a55c0103efb83fa4edfcbb8625f4c9e94ae8ec8e23827c563"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"filetime",
"log",
"shlex",
"thiserror 1.0.69",
]
[[package]]
name = "ens160"
version = "0.6.1"
@ -488,6 +625,22 @@ dependencies = [
"maybe-async-cfg",
]
[[package]]
name = "ens160-aq"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00bf02a6f1112f67da6a6638be8497415d7cf1ccba56d332889886108858f93"
dependencies = [
"bitfield 0.14.0",
"byteorder",
"embedded-hal 1.0.0",
"embuild",
"libm",
"log",
"maybe-async-cfg",
"num-traits",
]
[[package]]
name = "enumset"
version = "1.1.10"
@ -691,6 +844,26 @@ dependencies = [
"esp-metadata-generated",
]
[[package]]
name = "esp-rtos"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ec711c8d06e79c67b75d01595539e86b0aac209643af98ca87a12250428b3"
dependencies = [
"cfg-if",
"document-features",
"embassy-executor",
"embassy-sync 0.7.2",
"embassy-time-driver",
"embassy-time-queue-utils",
"esp-config",
"esp-hal",
"esp-hal-procmacros",
"esp-metadata-generated",
"esp-sync",
"portable-atomic",
]
[[package]]
name = "esp-sync"
version = "0.1.1"
@ -784,6 +957,65 @@ dependencies = [
"vcell",
]
[[package]]
name = "filetime"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.60.2",
]
[[package]]
name = "fixed"
version = "1.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707070ccf8c4173548210893a0186e29c266901b71ed20cd9e2ca0193dfe95c3"
dependencies = [
"az",
"bytemuck",
"half",
"typenum",
]
[[package]]
name = "fixed-macro"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0c48af8cb14e02868f449f8a2187bd78af7a08da201fdc78d518ecb1675bc"
dependencies = [
"fixed",
"fixed-macro-impl",
"fixed-macro-types",
]
[[package]]
name = "fixed-macro-impl"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c93086f471c0a1b9c5e300ea92f5cd990ac6d3f8edf27616ef624b8fa6402d4b"
dependencies = [
"fixed",
"paste",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "fixed-macro-types"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "044a61b034a2264a7f65aa0c3cd112a01b4d4ee58baace51fead3f21b993c7e4"
dependencies = [
"fixed",
"fixed-macro-impl",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
@ -855,6 +1087,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hash32"
version = "0.3.1"
@ -976,6 +1219,17 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
"redox_syscall",
]
[[package]]
name = "linked_list_allocator"
version = "0.10.5"
@ -1121,6 +1375,30 @@ dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
@ -1204,6 +1482,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "riscv"
version = "0.15.0"
@ -1331,6 +1618,12 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
@ -1358,6 +1651,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_cell"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
dependencies = [
"portable-atomic",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -1429,13 +1731,33 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.17",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
@ -1558,7 +1880,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1567,6 +1889,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -1576,6 +1907,71 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
@ -1616,3 +2012,23 @@ dependencies = [
"quote",
"syn 2.0.111",
]
[[package]]
name = "zerocopy"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]

View File

@ -6,6 +6,7 @@ version = "0.1.0"
[dependencies]
esp-hal = { version = "1.0.0", features = ["defmt", "esp32c3", "unstable"] }
esp-rtos = {version = "0.2.0", features = ["esp32c3", "embassy"]}
defmt = "1.0.1"
@ -31,6 +32,15 @@ buoyant = "0.5.3"
heapless = "0.9.2"
tinybmp = "0.6.0"
libm = "0.2.15"
embassy-executor = {version = "0.9.1", features = ["arch-riscv32"]}
embassy-time = "0.5.0"
embassy-sync = "0.7.2"
embedded-graphics-framebuf = "0.5.0"
static_cell = "2.1.1"
ens160-aq = {version = "0.2.11"}
aht20-async = "1.0.0"
fixed = "1.29.0"
fixed-macro = "1.2.0"
@ -45,5 +55,5 @@ debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 's'
opt-level = 3
overflow-checks = false

429
main.rs Normal file
View File

@ -0,0 +1,429 @@
#![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::cell::RefCell;
use core::default::Default;
use core::iter::Iterator;
use core::ops::Sub;
use critical_section::Mutex;
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::Io;
use esp_hal::gpio::Level;
use esp_hal::gpio::Output;
use esp_hal::gpio::OutputConfig;
use esp_hal::gpio::Pull;
use esp_hal::handler;
use esp_hal::peripherals;
use esp_hal::time::Rate;
use heapless::format;
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;
use crate::views::detail;
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!();
static INPUT_BUTTON: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
static BUTTON_PRESSED: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
#[main]
fn main() -> ! {
// generator version: 1.0.1
info!("Starting up.");
images::prepare_images();
info!("Prepared 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 mut io = Io::new(peripherals.IO_MUX);
info!("Init done.");
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,
);
info!("Display creating, initializing ...");
let mut display = mipidsi::Builder::new(ST7789, di)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut timer)
.unwrap();
info!("Initialized");
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();
info!("Creating sampler");
let mut sampler = Sampler::new(
peripherals.I2C0,
peripherals.GPIO8,
peripherals.GPIO9,
&mut timer,
);
let _ = sampler.sample(&mut timer);
info!("Sensor initialized");
// Input button interrupt
{
esp_hal::interrupt::enable(
peripherals::Interrupt::GPIO,
esp_hal::interrupt::Priority::Priority1,
)
.unwrap();
let mut input_button = Input::new(
peripherals.GPIO10,
InputConfig::default().with_pull(Pull::Up),
);
io.set_interrupt_handler(interrupt_handler);
critical_section::with(|cs| {
input_button.listen(esp_hal::gpio::Event::FallingEdge);
INPUT_BUTTON.borrow_ref_mut(cs).replace(input_button);
});
}
info!("Setup interrupts");
let mut history = History::new(&mut sampler, &mut timer);
let mut view_state = ViewState::Main;
let mut x = 0;
loop {
// input state
let button_pressed = critical_section::with(|cs| {
let val = *BUTTON_PRESSED.borrow(cs).borrow();
*BUTTON_PRESSED.borrow_ref_mut(cs) = false;
val
});
if button_pressed {
view_state = view_state.circulate();
info!("Btn pressed");
}
if false && (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
}
#[derive(Clone, Copy)]
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
},
}
}
}
#[derive(Clone, Copy)]
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 => {
detail::detailed(app_state, MenuIndicatorType::Temperature, target)
}
ViewState::GraphHumidity => {
detail::detailed(app_state, MenuIndicatorType::Humidity, target)
}
ViewState::GraphECo2 => detail::detailed(app_state, MenuIndicatorType::Co2, target),
ViewState::GraphTvoc => detail::detailed(app_state, MenuIndicatorType::Voc, target),
}
}
}
#[derive(Clone, Copy)]
pub enum MenuIndicatorType {
Temperature,
Humidity,
Co2,
Voc,
}
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, app_state: &AppState) -> heapless::String<16> {
match self {
MenuIndicatorType::Temperature => {
format!(16; "{:.1}", app_state.sample.temperature).unwrap()
}
MenuIndicatorType::Humidity => format!(16; "{:.1}", app_state.sample.humidity).unwrap(),
MenuIndicatorType::Co2 => format!(16; "{}", app_state.sample.eco2 as u32).unwrap(),
MenuIndicatorType::Voc => format!(16; "{}", app_state.sample.tvoc as u32).unwrap(),
}
}
pub fn get_tendency(&self, app_state: &AppState) -> Tendency {
match self {
MenuIndicatorType::Temperature => app_state.tendencies.temperature,
MenuIndicatorType::Humidity => app_state.tendencies.humidity,
MenuIndicatorType::Co2 => app_state.tendencies.eco2,
MenuIndicatorType::Voc => app_state.tendencies.tvoc,
}
}
}
#[derive(Clone, Copy)]
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)
}
#[handler]
fn interrupt_handler() {
critical_section::with(|cs| {
let mut button = INPUT_BUTTON.borrow_ref_mut(cs);
let Some(button) = button.as_mut() else {
return;
};
*BUTTON_PRESSED.borrow_ref_mut(cs) = true;
button.clear_interrupt();
});
}

64
src/display.rs Normal file
View File

@ -0,0 +1,64 @@
use defmt::info;
use embedded_hal_bus::spi::ExclusiveDevice;
use embedded_hal_bus::spi::NoDelay;
use esp_hal::Blocking;
use esp_hal::delay::Delay;
use esp_hal::gpio::Level;
use esp_hal::gpio::Output;
use esp_hal::gpio::OutputConfig;
use esp_hal::gpio::OutputPin;
use esp_hal::spi::master::Instance;
use esp_hal::spi::master::Spi;
use esp_hal::time::Rate;
use mipidsi::Display;
use mipidsi::NoResetPin;
use mipidsi::interface::SpiInterface;
use mipidsi::models::ST7789;
use static_cell::StaticCell;
pub type MainDisplay<'a> = Display<
SpiInterface<'a, ExclusiveDevice<Spi<'a, Blocking>, Output<'a>, NoDelay>, Output<'a>>,
ST7789,
NoResetPin,
>;
static SPI_BUFFER: StaticCell<[u8; 1024]> = StaticCell::new();
pub fn setup_display<'a>(
spi: impl Instance + 'static,
sck: impl OutputPin + 'static,
mosi: impl OutputPin + 'static,
cs: impl OutputPin + 'static,
dc: impl OutputPin + 'static,
timer: &mut Delay,
) -> MainDisplay<'a> {
let spi = esp_hal::spi::master::Spi::new(
spi,
esp_hal::spi::master::Config::default()
.with_mode(esp_hal::spi::Mode::_0)
.with_frequency(Rate::from_mhz(80)),
)
.unwrap()
.with_sck(sck)
.with_mosi(mosi);
let mut cs_output = Output::new(cs, Level::High, OutputConfig::default());
cs_output.set_high();
let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap();
// Define the display interface with no chip select
let spi_buffer = SPI_BUFFER.init([0u8; 1024]);
let di = SpiInterface::new(
spi_device,
Output::new(dc, Level::Low, OutputConfig::default()),
spi_buffer,
);
info!("Display creating, initializing ...");
let display = mipidsi::Builder::new(ST7789, di)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(timer)
.unwrap();
info!("Initialized");
display
}

View File

@ -8,58 +8,108 @@ use embedded_graphics::prelude::Point;
use embedded_graphics::prelude::Primitive;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::prelude::Size;
use embedded_graphics::prelude::WebColors;
use embedded_graphics::primitives::Line;
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics::primitives::line::StyledPixelsIterator;
use embedded_graphics::text::Text;
use embedded_graphics::text::renderer::TextRenderer;
use fixed::traits::Fixed;
use fixed::types::I16F16;
use fixed_macro::fixed;
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 {
use crate::colors::BACKGROUND_COLOR;
pub type FixedType = I16F16;
fn map_float(
x: FixedType,
x_min: FixedType,
x_max: FixedType,
y_min: FixedType,
y_max: FixedType,
) -> FixedType {
((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];
pub const TEMPERATURE_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),
];
pub const HUMIDITY_LUT: [Rgb565; 2] = [Rgb565::CSS_DIM_GRAY, Rgb565::new(0, 14, 29)];
pub const ECO2_LUT: [Rgb565; 3] = [Rgb565::GREEN, Rgb565::CSS_ORANGE, Rgb565::RED];
pub const TVOC_LUT: [Rgb565; 3] = [Rgb565::GREEN, Rgb565::CSS_ORANGE, Rgb565::RED];
fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: f32) -> Rgb565 {
#[derive(Clone, Copy)]
pub enum Lut {
HeightLut(&'static [Rgb565]),
MapLut(&'static [Rgb565], FixedType, FixedType),
}
impl Lut {
pub fn get_color(&self, height_factor: FixedType, value: FixedType) -> Rgb565 {
match self {
Lut::HeightLut(lut) => color_lut(height_factor, lut),
Lut::MapLut(lut, min, max) => color_lut(
map_float(
value.clamp(*min, *max),
*min,
*max,
FixedType::ZERO,
FixedType::ONE,
),
lut,
),
}
}
}
fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: FixedType) -> 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,
(FixedType::from_num(a.r()) * (FixedType::from_num(1) - x)).to_num::<u8>()
+ (FixedType::from_num(b.r()) * x).to_num::<u8>(),
(FixedType::from_num(a.g()) * (FixedType::from_num(1) - x)).to_num::<u8>()
+ (FixedType::from_num(b.g()) * x).to_num::<u8>(),
(FixedType::from_num(a.b()) * (FixedType::from_num(1) - x)).to_num::<u8>()
+ (FixedType::from_num(b.b()) * x).to_num::<u8>(),
)
}
fn color_lut(mut x: f32, colors: &[Rgb565]) -> Rgb565 {
if x == 1. {
fn color_lut(mut x: FixedType, colors: &[Rgb565]) -> Rgb565 {
if x == FixedType::from_num(1.) {
return colors[colors.len() - 1];
}
if x == 0. {
if x == FixedType::from_num(0.) {
return colors[0];
}
x *= (colors.len() - 1) as f32;
x *= FixedType::from_num(colors.len() - 1);
let index = libm::floorf(x);
let index = x.floor();
let interp = x - index;
rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp)
rgb565_interpolate(
colors[index.to_num::<usize>()],
colors[index.to_num::<usize>() + 1],
interp.to_num(),
)
}
pub fn graph_data<I, T>(data: I, data_count: usize, target: &mut T)
where
I: Iterator<Item = f32> + Clone,
pub fn graph_data<I, T>(
data: I,
min: FixedType,
max: FixedType,
lut: Lut,
data_count: usize,
target: &mut T,
) where
I: Iterator<Item = FixedType> + 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,
@ -70,16 +120,32 @@ where
0,
map_float(
data.clone().next().unwrap(),
min,
max,
size.height as f32,
0.,
) as i32,
FixedType::from_num(min),
FixedType::from_num(max),
FixedType::from_num(size.height as f32),
FixedType::from_num(0.),
)
.to_num::<i32>(),
);
for (i, x) in data.clone().skip(1).enumerate() {
for (i, x) in data.enumerate().skip(1) {
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,
map_float(
FixedType::from_num(i),
FixedType::from_num(0),
FixedType::from_num(data_count - 1),
FixedType::from_num(0),
FixedType::from_num(size.width),
)
.to_num::<i32>(),
map_float(
x,
min,
max,
FixedType::from_num(size.height),
FixedType::from_num(0),
)
.to_num::<i32>(),
);
let _ = Line::new(start, point)
.into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 2))
@ -94,16 +160,38 @@ where
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);
let height_factor = map_float(
FixedType::from_num(y),
FixedType::from_num(0),
FixedType::from_num(size.height),
FixedType::from_num(1),
FixedType::from_num(0.),
);
//let height_color = color_lut(height_factor, &DEFAULT_LUT);
let height_color = lut.get_color(
height_factor,
map_float(
FixedType::from_num(y),
FixedType::from_num(size.height),
FixedType::from_num(0),
min,
max,
),
);
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 {
} else if met_curve && (x as i32 - 2 * y as i32) % 7 == 0 {
let _ = Pixel(
position,
rgb565_interpolate(height_color, Rgb565::BLACK, 1. - height_factor),
rgb565_interpolate(
height_color,
Rgb565::BLACK,
FixedType::from_num(1) - FixedType::from_num(height_factor),
),
)
.draw(target);
}
@ -111,154 +199,154 @@ where
}
}
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);
}
// 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

@ -5,193 +5,280 @@
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)]
use core::cell::RefCell;
use core::hint;
use core::ops::Add;
use buoyant::primitives::ProposedDimension;
use buoyant::view::AsDrawable;
use critical_section::Mutex;
use defmt::info;
use defmt::trace;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_time::Duration;
use embassy_time::Timer;
use embassy_time::with_timeout;
use embedded_graphics::Drawable;
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::framebuffer::Framebuffer;
use embedded_graphics::framebuffer::buffer_size;
use embedded_graphics::geometry::Dimensions;
use embedded_graphics::image::GetPixel;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::pixelcolor::raw::LittleEndian;
use embedded_graphics::prelude::PixelColor;
use embedded_graphics::prelude::Primitive;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics_framebuf::FrameBuf;
use embedded_graphics_framebuf::backends::FrameBufferBackend;
use esp_hal::clock::CpuClock;
use esp_hal::gpio::Input;
use esp_hal::gpio::InputConfig;
use esp_hal::gpio::Pull;
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::timer::timg::TimerGroup;
use esp_rtos::main;
extern crate alloc;
extern crate esp_alloc;
mod colors;
mod display;
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;
esp_bootloader_esp_idf::esp_app_desc!();
use esp_backtrace as _;
use esp_hal::main;
use esp_println as _;
use heapless::HistoryBuf;
use heapless::format;
use static_cell::StaticCell;
use crate::colors::BACKGROUND_COLOR;
use crate::graph::graph_data;
use crate::graph::max_indicator;
use crate::graph::min_indicator;
use crate::display::MainDisplay;
use crate::images::StaticImage;
use crate::sampler::History;
use crate::sampler::Sample;
use crate::sampler::Sampler;
extern crate alloc;
pub enum ApplicationEvent {
ButtonPress,
LongButtonPress,
NewSample(Sample),
}
// 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!();
static EVENT_CHANNEL: Channel<CriticalSectionRawMutex, ApplicationEvent, 8> = Channel::new();
static MAIN_DISPLAY: Mutex<RefCell<Option<MainDisplay>>> = Mutex::new(RefCell::new(None));
static SAMPLER: StaticCell<Sampler> = StaticCell::new();
#[main]
fn main() -> ! {
// generator version: 1.0.1
images::prepare_images();
async fn main(spawner: Spawner) {
esp_alloc::heap_allocator!(size: 32 * 1024);
// generator version: 1.0.1
info!("Starting up.");
images::prepare_images();
info!("Prepared images.");
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let mut timer = Delay::new();
let timg0 = TimerGroup::new(peripherals.TIMG0);
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, software_interrupt.software_interrupt0);
let spi = esp_hal::spi::master::Spi::new(
info!("Init done.");
info!("Setting up display");
let mut timer = esp_hal::delay::Delay::new();
let mut display = display::setup_display(
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,
peripherals.GPIO4,
peripherals.GPIO6,
peripherals.GPIO0,
peripherals.GPIO1,
&mut timer,
);
let mut display = mipidsi::Builder::new(ST7789, di)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut timer)
.unwrap();
info!("Clearing screen");
{
let fbuf_data = [Rgb565::new(0, 0, 0); 240 * 240];
let _ = display.fill_contiguous(&display.bounding_box(), fbuf_data);
}
critical_section::with(|cs| {
MAIN_DISPLAY.borrow_ref_mut(cs).replace(display);
});
let mut fb =
Framebuffer::<Rgb565, _, LittleEndian, 240, 240, { buffer_size::<Rgb565>(240, 240) }>::new(
);
// Setup button
let btn = Input::new(
peripherals.GPIO10,
InputConfig::default().with_pull(Pull::Down),
);
// 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(
// Setup sampler
let sampler = SAMPLER.init(Sampler::new(
peripherals.I2C0,
peripherals.GPIO8,
peripherals.GPIO9,
&mut timer,
);
timer.delay_millis(2000);
let _ = sampler.sample(&mut timer);
));
sampler.sample(&mut timer);
Timer::after_secs(2).await;
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
spawner.spawn(button_listener(btn)).unwrap();
spawner.spawn(event_handler()).unwrap();
spawner.spawn(sampler_task(sampler)).unwrap();
}
#[embassy_executor::task]
async fn button_listener(mut btn: Input<'static>) {
info!("Button listner task launched");
let sender = EVENT_CHANNEL.sender();
loop {
btn.wait_for_rising_edge().await;
match with_timeout(Duration::from_millis(750), btn.wait_for_low()).await {
Ok(()) => {
info!("Short press");
sender.send(ApplicationEvent::ButtonPress).await
}
Err(_) => {
info!("Long press");
sender.send(ApplicationEvent::LongButtonPress).await;
btn.wait_for_low().await
}
};
Timer::after_millis(20).await;
}
}
#[embassy_executor::task]
async fn event_handler() {
info!("Event handler task launched");
let receiver = EVENT_CHANNEL.receiver();
let mut display = critical_section::with(|cs| MAIN_DISPLAY.borrow_ref_mut(cs).take().unwrap());
let mut fbuf =
Framebuffer::<Rgb565, _, LittleEndian, 240, 240, { buffer_size::<Rgb565>(240, 240) }>::new(
);
let mut last_5_mins: HistoryBuf<Sample, { 60 * 5 }> = HistoryBuf::new();
let mut redraw = true;
let mut current_view = ViewState::Main;
loop {
if redraw {
redraw = false;
let mut draw_graph = None;
let tendencies = Tendencies::from_history(&last_5_mins);
let _ = fbuf
.bounding_box()
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
.draw(&mut fbuf);
match current_view {
ViewState::Main => {
let _ = views::menu::menu_view(
*last_5_mins.last().unwrap_or(&Sample::zero()),
tendencies,
)
.as_drawable(fbuf.bounding_box().size, Rgb565::WHITE)
.draw(&mut fbuf);
}
ViewState::TemperatureGraph => draw_graph = Some(MeasurementType::Temperature),
ViewState::HumidityGraph => draw_graph = Some(MeasurementType::Humidity),
ViewState::ECo2Graph => draw_graph = Some(MeasurementType::ECo2),
ViewState::TVocGraph => draw_graph = Some(MeasurementType::TVoc),
}
if let Some(graph_type) = draw_graph {
views::detail::detailed(&last_5_mins, tendencies, graph_type, &mut fbuf);
}
let _ = display.fill_contiguous(
&fbuf.bounding_box(),
fbuf.data()
.chunks(2)
.map(|x| unsafe { core::mem::transmute::<_, Rgb565>([x[0], x[1]]) }),
);
}
let event = receiver.receive().await;
match event {
ApplicationEvent::ButtonPress => {
current_view = current_view.next();
redraw = true;
}
ApplicationEvent::NewSample(x) => {
last_5_mins.write(x);
redraw = true;
}
ApplicationEvent::LongButtonPress => {
redraw = true;
}
}
}
}
#[derive(Clone, Copy)]
pub enum ViewState {
Main,
TemperatureGraph,
HumidityGraph,
ECo2Graph,
TVocGraph,
}
impl ViewState {
pub fn next(&self) -> Self {
match self {
ViewState::Main => ViewState::TemperatureGraph,
ViewState::TemperatureGraph => ViewState::HumidityGraph,
ViewState::HumidityGraph => ViewState::ECo2Graph,
ViewState::ECo2Graph => ViewState::TVocGraph,
ViewState::TVocGraph => ViewState::Main,
}
}
}
const LOW_PASS_LENGTH: usize = 5;
#[embassy_executor::task]
async fn sampler_task(sampler: &'static mut Sampler<'static>) {
info!("Sampler task launched");
let sender = EVENT_CHANNEL.sender();
sender
.send(ApplicationEvent::NewSample(Sample::zero()))
.await;
let mut low_pass: HistoryBuf<Sample, LOW_PASS_LENGTH> = HistoryBuf::new();
let mut delay = esp_hal::delay::Delay::new();
let mut count = 0;
loop {
Timer::after(Duration::from_millis(500)).await;
let sample = sampler.sample(&mut delay);
low_pass.write(sample);
count += 1;
if count >= 2 {
sender
.send(ApplicationEvent::NewSample(
low_pass
.oldest_ordered()
.copied()
.reduce(Sample::add)
.unwrap_or(Sample::zero())
* (1. / LOW_PASS_LENGTH as f32),
))
.await;
count = 0;
}
}
}
#[derive(Clone, Copy)]
pub struct Tendencies {
temperature: Tendency,
humidity: Tendency,
@ -200,9 +287,9 @@ pub struct Tendencies {
}
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);
pub fn from_history<const N: usize>(history: &HistoryBuf<Sample, N>) -> Self {
let mut iter = history.oldest_ordered().rev().copied().take(5);
let len = history.len().min(5);
if len <= 1 {
return Tendencies {
@ -259,113 +346,7 @@ impl Tendencies {
}
}
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,
@ -382,14 +363,48 @@ impl Tendency {
}
}
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)
#[derive(Clone, Copy)]
pub enum MeasurementType {
Temperature,
Humidity,
ECo2,
TVoc,
}
impl MeasurementType {
pub fn get_corresponding_icon(&self) -> &'static StaticImage {
match self {
MeasurementType::Temperature => &images::TEMPERATURE_ICON,
MeasurementType::Humidity => &images::HUMIDITY_ICON,
MeasurementType::ECo2 => &images::CO2_ICON,
MeasurementType::TVoc => &images::VOC_ICON,
}
}
pub fn get_corresponding_unit_string(&self) -> &'static str {
match self {
MeasurementType::Temperature => "C",
MeasurementType::Humidity => "%",
MeasurementType::ECo2 => "ppm",
MeasurementType::TVoc => "ppb",
}
}
pub fn get_value_str(&self, sample: Sample) -> heapless::String<16> {
match self {
MeasurementType::Temperature => format!(16; "{:.1}", sample.temperature).unwrap(),
MeasurementType::Humidity => format!(16; "{:.1}", sample.humidity).unwrap(),
MeasurementType::ECo2 => format!(16; "{}", sample.eco2 as u32).unwrap(),
MeasurementType::TVoc => format!(16; "{}", sample.tvoc as u32).unwrap(),
}
}
pub fn get_tendency(&self, tendencies: Tendencies) -> Tendency {
match self {
MeasurementType::Temperature => tendencies.temperature,
MeasurementType::Humidity => tendencies.humidity,
MeasurementType::ECo2 => tendencies.eco2,
MeasurementType::TVoc => tendencies.tvoc,
}
}
}

View File

@ -5,26 +5,19 @@ 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_aq::Ens160;
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>>>,
@ -133,6 +126,7 @@ impl<'a> Sampler<'a> {
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,

View File

@ -1,31 +1,132 @@
use buoyant::layout::HorizontalAlignment;
use buoyant::layout::VerticalAlignment;
use buoyant::primitives::Size;
use buoyant::view::AsDrawable;
use buoyant::view::HStack;
use buoyant::view::Spacer;
use buoyant::view::Text;
use buoyant::view::VStack;
use buoyant::view::View;
use buoyant::view::ViewExt;
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::Rgb565;
use embedded_graphics::pixelcolor::raw::LittleEndian;
use embedded_graphics::prelude::DrawTarget;
use embedded_graphics::prelude::RgbColor;
use fixed::types::I16F16;
use heapless::HistoryBuf;
use profont::PROFONT_18_POINT;
use profont::PROFONT_24_POINT;
use crate::MenuIndicatorType;
use crate::Tendency;
use crate::MeasurementType;
use crate::Tendencies;
use crate::colors::FRAME_STROKE_COLOR;
use crate::colors::MAIN_TEXT_COLOR;
use crate::colors::SUB_TEXT_COLOR;
use crate::tendency_indicator;
use crate::graph::ECO2_LUT;
use crate::graph::FixedType;
use crate::graph::HUMIDITY_LUT;
use crate::graph::Lut;
use crate::graph::TEMPERATURE_LUT;
use crate::graph::graph_data;
//use crate::graph::max_indicator;
//use crate::graph::min_indicator;
use crate::sampler::Sample;
use crate::views::icon::icon_box_view;
use crate::views::menu::tendency_indicator;
pub fn detailed_view(indicator: MenuIndicatorType, tendency: Tendency) -> impl View<Rgb565> {
pub fn detailed<T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>, const N: usize>(
history: &HistoryBuf<Sample, N>,
tendencies: Tendencies,
indicator: MeasurementType,
target: &mut T,
) {
let _ = detailed_view_top(indicator, tendencies, *history.last().unwrap())
.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 = history
.oldest_ordered()
.map(|x| match indicator {
MeasurementType::Temperature => x.temperature,
MeasurementType::Humidity => x.humidity,
MeasurementType::ECo2 => x.eco2,
MeasurementType::TVoc => x.tvoc,
})
.map(I16F16::from_num);
let min = iter
.clone()
.reduce(Ord::min)
.unwrap_or(FixedType::from_num(0.));
let max = iter
.clone()
.reduce(Ord::max)
.unwrap_or(FixedType::from_num(0.));
let min_spacing = FixedType::from_num(match indicator {
MeasurementType::Temperature => 5,
MeasurementType::Humidity => 10,
MeasurementType::ECo2 => 100,
MeasurementType::TVoc => 100,
});
let middle = (max + min) / FixedType::from_num(2.);
let min = middle - min_spacing.max(max - min) / FixedType::from_num(2.);
let max = middle + min_spacing.max(max - min) / FixedType::from_num(2.);
let lut = match indicator {
MeasurementType::Temperature => Lut::HeightLut(&TEMPERATURE_LUT),
MeasurementType::Humidity => Lut::HeightLut(&HUMIDITY_LUT),
MeasurementType::ECo2 => Lut::MapLut(
&ECO2_LUT,
FixedType::from_num(500),
FixedType::from_num(1500),
),
MeasurementType::TVoc => Lut::MapLut(
&ECO2_LUT,
FixedType::from_num(200),
FixedType::from_num(1000),
),
};
graph_data(iter.clone(), min, max, lut, history.len(), &mut graph_fb);
//min_indicator(iter.clone(), history.len(), &mut graph_fb);
//max_indicator(iter.clone(), history.len(), &mut graph_fb);
let img_raw = ImageRaw::<Rgb565, LittleEndian>::new(graph_fb.data(), 240);
let image = embedded_graphics::image::Image::new(
&img_raw,
embedded_graphics::prelude::Point::new(0, 53),
);
let _ = image.draw(target);
}
pub fn detailed_view_top(
indicator: MeasurementType,
tendencies: Tendencies,
sample: Sample,
) -> impl View<Rgb565> {
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)
tendency_indicator(indicator.get_tendency(tendencies)),
Text::new(indicator.get_value_str(sample), &PROFONT_24_POINT)
.foreground_color(MAIN_TEXT_COLOR),
Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT)
.foreground_color(SUB_TEXT_COLOR)
@ -34,13 +135,7 @@ pub fn detailed_view(indicator: MenuIndicatorType, tendency: Tendency) -> impl V
.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),
//Spacer::default().flex_frame().with_max_width(10),
)),
// Window
Spacer::default()

View File

@ -7,8 +7,8 @@ use embedded_graphics::prelude::*;
use profont::PROFONT_18_POINT;
use profont::PROFONT_24_POINT;
use crate::AppState;
use crate::MenuIndicatorType;
use crate::MeasurementType;
use crate::Tendencies;
use crate::Tendency;
use crate::colors::FRAME_BACKGROUD_COLOR;
use crate::colors::FRAME_STROKE;
@ -16,37 +16,30 @@ use crate::colors::FRAME_STROKE_COLOR;
use crate::colors::MAIN_TEXT_COLOR;
use crate::colors::SUB_TEXT_COLOR;
use crate::get_image;
use crate::sampler::Sample;
use crate::views::icon::icon_box_view;
pub fn menu_view(app_state: AppState) -> impl View<Rgb565> {
pub fn menu_view(sample: Sample, tendencies: Tendencies) -> impl View<Rgb565> {
VStack::new((
HStack::new((
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,
),
main_menu_indicator(MeasurementType::Temperature, tendencies, sample),
main_menu_indicator(MeasurementType::Humidity, tendencies, sample),
))
.with_spacing(2),
HStack::new((
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,
),
main_menu_indicator(MeasurementType::ECo2, tendencies, sample),
main_menu_indicator(MeasurementType::TVoc, tendencies, sample),
))
.with_spacing(2),
))
.with_spacing(2)
}
fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) -> impl View<Rgb565> {
fn main_menu_indicator(
indicator_type: MeasurementType,
tendencies: Tendencies,
sample: Sample,
) -> impl View<Rgb565> {
Rectangle
.corner_radius(5)
.stroked(FRAME_STROKE)
@ -64,8 +57,8 @@ fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) ->
)),
HStack::new((
Spacer::default(),
tendency_indicator(tendency),
Text::new(indicator_type.get_value_str(), &PROFONT_24_POINT)
tendency_indicator(indicator_type.get_tendency(tendencies)),
Text::new(indicator_type.get_value_str(sample), &PROFONT_24_POINT)
.foreground_color(MAIN_TEXT_COLOR),
Text::new(
indicator_type.get_corresponding_unit_string(),
@ -85,7 +78,7 @@ fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) ->
})
}
fn tendency_indicator(tendency: Tendency) -> impl View<Rgb565> {
pub fn tendency_indicator(tendency: Tendency) -> impl View<Rgb565> {
HStack::new((
Image::new(get_image!(tendency.get_corresponding_icon()))
.flex_frame()