Compare commits
6 Commits
db7b6c5937
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 43fead021c | |||
| 56f9c423e7 | |||
| 9925499eff | |||
| 71c1dc4fd9 | |||
| ea9852d5bc | |||
| 3cb8a04a77 |
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "aht20-driver"]
|
||||
path = aht20-driver
|
||||
url = git@github.com:chalbin73/aht20-driver.git
|
||||
479
Cargo.lock
generated
@ -2,11 +2,20 @@
|
||||
# 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c920c092aef3156141a32df93663d683f06c733187f924b09d917efa53f08fb"
|
||||
dependencies = [
|
||||
"crc-any",
|
||||
"embedded-hal 1.0.0",
|
||||
@ -18,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"
|
||||
@ -74,6 +89,19 @@ version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "buoyant"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc36827a22794f33600a539fc2f16e6c535f9de5cf26509f8ce7ddb666cda1b"
|
||||
dependencies = [
|
||||
"embedded-graphics",
|
||||
"embedded-graphics-core",
|
||||
"heapless 0.8.0",
|
||||
"paste",
|
||||
"u8g2-fonts",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
@ -96,20 +124,33 @@ 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",
|
||||
"lcd-async",
|
||||
"esp-rtos",
|
||||
"fixed",
|
||||
"fixed-macro",
|
||||
"heapless 0.9.2",
|
||||
"libm",
|
||||
"mipidsi",
|
||||
"profont",
|
||||
"static_cell",
|
||||
"tinybmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -124,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"
|
||||
@ -247,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]]
|
||||
@ -296,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"
|
||||
@ -343,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"
|
||||
@ -352,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"
|
||||
@ -375,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"
|
||||
@ -463,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"
|
||||
@ -474,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"
|
||||
@ -677,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"
|
||||
@ -770,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"
|
||||
@ -841,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"
|
||||
@ -950,24 +1207,29 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lcd-async"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "362f088c5c176ceedd4d0e817d17ae3b79480495deac605158a7cd86450450c5"
|
||||
dependencies = [
|
||||
"embedded-graphics",
|
||||
"embedded-graphics-core",
|
||||
"embedded-hal 1.0.0",
|
||||
"embedded-hal-async",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
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"
|
||||
@ -1113,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"
|
||||
@ -1196,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"
|
||||
@ -1323,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"
|
||||
@ -1350,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"
|
||||
@ -1421,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]]
|
||||
@ -1441,6 +1771,15 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinybmp"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df43af2cb7b369009aa14144959bb4f2720ab62034c9073242f2d3a186c2edb6"
|
||||
dependencies = [
|
||||
"embedded-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.3"
|
||||
@ -1477,6 +1816,16 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "u8g2-fonts"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "825f57be1429fd60f335a4aade11e128a7ae4f89d75ca3a003cb8410a91093f7"
|
||||
dependencies = [
|
||||
"embedded-graphics",
|
||||
"embedded-graphics-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ufmt-write"
|
||||
version = "0.1.0"
|
||||
@ -1531,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]]
|
||||
@ -1540,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"
|
||||
@ -1549,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"
|
||||
@ -1589,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",
|
||||
]
|
||||
|
||||
27
Cargo.toml
@ -4,12 +4,9 @@ name = "co2-meter"
|
||||
rust-version = "1.88"
|
||||
version = "0.1.0"
|
||||
|
||||
[[bin]]
|
||||
name = "co2-meter"
|
||||
path = "./src/bin/main.rs"
|
||||
|
||||
[dependencies]
|
||||
esp-hal = { version = "1.0.0", features = ["defmt", "esp32c3", "unstable"] }
|
||||
esp-rtos = {version = "0.2.0", features = ["esp32c3", "embassy"]}
|
||||
|
||||
|
||||
defmt = "1.0.1"
|
||||
@ -23,14 +20,28 @@ esp-backtrace = { version = "0.18.1", features = [
|
||||
"panic-handler",
|
||||
] }
|
||||
esp-println = { version = "0.16.1", features = ["defmt-espflash", "esp32c3"] }
|
||||
aht20-driver = { version = "2.0", default-features = false }
|
||||
|
||||
aht20-driver = { path = "aht20-driver" , default-features = false }
|
||||
ens160 = { version = "0.6", default-features = false}
|
||||
embedded-hal-bus = "0.3.0"
|
||||
embedded-hal-bus = {version = "0.3.0", features = ["alloc"]}
|
||||
mipidsi = "0.9.0"
|
||||
embedded-graphics = "0.8.1"
|
||||
lcd-async = "0.1.1"
|
||||
|
||||
profont = "0.7.0"
|
||||
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"
|
||||
|
||||
|
||||
|
||||
[profile.dev]
|
||||
@ -44,5 +55,5 @@ debug = 2
|
||||
debug-assertions = false
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 's'
|
||||
opt-level = 3
|
||||
overflow-checks = false
|
||||
|
||||
1
aht20-driver
Submodule
18
assets/Makefile
Normal file
@ -0,0 +1,18 @@
|
||||
SVGS = $(wildcard ./*.svg)
|
||||
TARGET_BMPS = $(SVGS:.svg=.bmp)
|
||||
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 $< -o $@
|
||||
|
||||
BIN
assets/a.bmp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
52
assets/a.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="45"
|
||||
height="45"
|
||||
viewBox="0 0 28.421052 28.421053"
|
||||
stroke-width="1.6"
|
||||
fill="none"
|
||||
color="#bababa"
|
||||
data-darkreader-inline-color=""
|
||||
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.942621"
|
||||
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" />
|
||||
<rect
|
||||
style="fill:#242424;stroke-width:11.5955;paint-order:markers stroke fill;fill-opacity:1"
|
||||
id="rect1"
|
||||
width="28.421053"
|
||||
height="28.421053"
|
||||
x="-2.1636963e-07"
|
||||
y="-2.1636963e-07" />
|
||||
<path
|
||||
d="m 23.127726,16.838624 c 0,-5.022692 -9.094383,-13.6415758 -9.094383,-13.6415758 0,0 -9.0943834,8.6188838 -9.0943834,13.6415758 0,5.022714 4.0716919,9.094383 9.0943834,9.094383 5.022714,0 9.094383,-4.071669 9.094383,-9.094383 z"
|
||||
stroke="#bababa"
|
||||
stroke-width="1.6"
|
||||
data-darkreader-inline-stroke=""
|
||||
style="stroke:#0137f1;stroke-width:4.04437;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/co2-icon.bmp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
89
assets/co2-icon.svg
Normal file
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="45"
|
||||
height="45"
|
||||
viewBox="0 0 11.90625 11.90625"
|
||||
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="7.6922777"
|
||||
inkscape:cx="29.575115"
|
||||
inkscape:cy="28.27511"
|
||||
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"
|
||||
transform="translate(0.08272917,0.15224179)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.6006;stroke-dasharray:6.40237, 3.20118, 1.6006, 3.20118;paint-order:markers stroke fill"
|
||||
id="rect1"
|
||||
width="11.90625"
|
||||
height="11.90625"
|
||||
x="-0.082729168"
|
||||
y="-0.1522418" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.74984px;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:0.296867;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
|
||||
x="9.2658949"
|
||||
y="8.814003"
|
||||
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:4.74984px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:0.296867"
|
||||
x="9.2658949"
|
||||
y="8.814003">2</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.34054px;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:0.458785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
|
||||
x="0.056607485"
|
||||
y="9.7028713"
|
||||
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:7.34054px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:0.458785"
|
||||
x="0.056607485"
|
||||
y="9.7028713">C</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.34054px;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:0.458785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2"
|
||||
x="4.6601677"
|
||||
y="7.3943286"
|
||||
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:7.34054px;font-family:'ProFont IIx Nerd Font Propo';-inkscape-font-specification:'ProFont IIx Nerd Font Propo';fill:#00ade7;fill-opacity:1;stroke-width:0.458785"
|
||||
x="4.6601677"
|
||||
y="7.3943286">O</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
BIN
assets/humidity-icon.bmp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
60
assets/humidity-icon.svg
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg:svg
|
||||
width="45"
|
||||
height="45"
|
||||
viewBox="0 0 28.421053 28.421053"
|
||||
stroke-width="1.6"
|
||||
fill="none"
|
||||
color="#bababa"
|
||||
data-darkreader-inline-color=""
|
||||
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:svg="http://www.w3.org/2000/svg">
|
||||
<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.890141"
|
||||
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" />
|
||||
<svg:rect
|
||||
style="fill:#000000;stroke-width:6.22986;stroke-dasharray:none;paint-order:markers stroke fill;fill-opacity:1"
|
||||
id="rect1"
|
||||
width="28.421053"
|
||||
height="28.421053"
|
||||
x="3.3630371e-08"
|
||||
y="3.3630371e-08" />
|
||||
<svg:path
|
||||
d="m 23.392462,16.891615 c 0,-5.071045 -9.181935,-13.7729019 -9.181935,-13.7729019 0,0 -9.1819355,8.7018569 -9.1819355,13.7729019 0,5.071068 4.1108899,9.181935 9.1819355,9.181935 5.071068,0 9.181935,-4.110867 9.181935,-9.181935 z"
|
||||
stroke="#bababa"
|
||||
stroke-width="1.6"
|
||||
data-darkreader-inline-stroke=""
|
||||
style="stroke:#0137f1;stroke-width:4.0833;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1" />
|
||||
<style
|
||||
class="darkreader darkreader--fallback">html, body, body :not(iframe) {
|
||||
background-color: var(--darkreader-background-ffffff, #181a1b) !important;
|
||||
border-color: var(--darkreader-border-404040, #776e62) !important;
|
||||
color: var(--darkreader-text-000000, #e8e6e3) !important;
|
||||
}
|
||||
div[style*="background-color: rgb(135, 135, 135)"] {
|
||||
background-color: #878787 !important;
|
||||
}</style>
|
||||
</svg:svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/indic-falling.bmp
Normal file
|
After Width: | Height: | Size: 538 B |
94
assets/indic-falling.svg
Normal file
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="10"
|
||||
height="20"
|
||||
viewBox="0 0 10 20"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="indic-falling.svg"
|
||||
inkscape:export-filename="indic-rising.svg"
|
||||
inkscape:export-xdpi="102.28008"
|
||||
inkscape:export-ydpi="102.28008"
|
||||
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:export-bgcolor="#080808ff"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="3.640625"
|
||||
inkscape:cy="8.328125"
|
||||
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"
|
||||
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect4"
|
||||
width="98.624123"
|
||||
height="197.24825"
|
||||
x="-44.312061"
|
||||
y="-1.4580333" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="-12.4031"
|
||||
transform="matrix(0.82051985,0,0,0.88816373,-16.031162,29.570393)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#db0000;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1-1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="12.403106"
|
||||
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
|
||||
<circle
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:#000000;stroke-width:5.96404155;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path2"
|
||||
cx="5.0000005"
|
||||
cy="97.166092"
|
||||
r="43.119678" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/indic-rising.bmp
Normal file
|
After Width: | Height: | Size: 538 B |
94
assets/indic-rising.svg
Normal file
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="10"
|
||||
height="20"
|
||||
viewBox="0 0 10 20"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="indic-rising.svg"
|
||||
inkscape:export-filename="indic-rising.svg"
|
||||
inkscape:export-xdpi="102.28008"
|
||||
inkscape:export-ydpi="102.28008"
|
||||
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:export-bgcolor="#080808ff"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="3.640625"
|
||||
inkscape:cy="8.328125"
|
||||
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"
|
||||
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect4"
|
||||
width="98.624123"
|
||||
height="197.24825"
|
||||
x="-44.312061"
|
||||
y="-1.4580333" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#00e565;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="-12.4031"
|
||||
transform="matrix(0.82051985,0,0,0.88816373,-16.031162,29.570393)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1-1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="12.403106"
|
||||
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
|
||||
<circle
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:#000000;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path2"
|
||||
cx="5.0000005"
|
||||
cy="97.166092"
|
||||
r="43.119678" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/indic-steady.bmp
Normal file
|
After Width: | Height: | Size: 538 B |
94
assets/indic-steady.svg
Normal file
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="10"
|
||||
height="20"
|
||||
viewBox="0 0 10 20"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="indic-steady.svg"
|
||||
inkscape:export-filename="indic-rising.svg"
|
||||
inkscape:export-xdpi="102.28008"
|
||||
inkscape:export-ydpi="102.28008"
|
||||
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:export-bgcolor="#080808ff"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="3.640625"
|
||||
inkscape:cy="8.328125"
|
||||
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"
|
||||
transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:95.2216;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect4"
|
||||
width="98.624123"
|
||||
height="197.24825"
|
||||
x="-44.312061"
|
||||
y="-1.4580333" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="-12.4031"
|
||||
transform="matrix(0.82051985,0,0,0.88816373,-16.031162,29.570393)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1c1c1c;fill-opacity:1;stroke:none;stroke-width:6.94267343;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path1-1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="25.63151"
|
||||
sodipodi:cy="24.391275"
|
||||
sodipodi:r1="55.859524"
|
||||
sodipodi:r2="27.929762"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0.1"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.007276,52.321038 c -4.837576,8.378928 -91.913956,8.378928 -96.751533,-1e-6 -4.837577,-8.378929 38.700614,-83.789285 48.375767,-83.789285 9.675153,0 53.213343,75.410357 48.375766,83.789286 z"
|
||||
inkscape:transform-center-y="12.403106"
|
||||
transform="matrix(-0.82051985,0,0,-0.88816373,26.031162,164.76177)" />
|
||||
<circle
|
||||
style="fill:#f6ab00;fill-opacity:1;stroke:#000000;stroke-width:5.97976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0.2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path2"
|
||||
cx="5.0000005"
|
||||
cy="97.166092"
|
||||
r="43.119678" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/temperature-icon.bmp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
116
assets/temperature-icon.svg
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg:svg
|
||||
width="45"
|
||||
height="45"
|
||||
viewBox="0 0 45 45"
|
||||
stroke-width="1.5"
|
||||
fill="none"
|
||||
color="#ff6600"
|
||||
data-darkreader-inline-color=""
|
||||
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:svg="http://www.w3.org/2000/svg">
|
||||
<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="10.216728"
|
||||
inkscape:cx="24.910128"
|
||||
inkscape:cy="27.74861"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1032"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<style
|
||||
class="darkreader darkreader--fallback">html, body, body :not(iframe) {
|
||||
background-color: var(--darkreader-background-ffffff, #181a1b) !important;
|
||||
border-color: var(--darkreader-border-404040, #776e62) !important;
|
||||
color: var(--darkreader-text-000000, #e8e6e3) !important;
|
||||
}
|
||||
div[style*="background-color: rgb(135, 135, 135)"] {
|
||||
background-color: #878787 !important;
|
||||
}</style>
|
||||
<svg:g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
transform="matrix(2.0390253,0,0,2.0390253,-3.3768322,-2.3378069)">
|
||||
<svg:rect
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:5.46432;paint-order:markers stroke fill"
|
||||
id="rect1"
|
||||
width="22.069368"
|
||||
height="22.069368"
|
||||
x="1.6561011"
|
||||
y="1.1465315" />
|
||||
<svg:path
|
||||
d="m 6,11.9995 c -1.21445,0.9122 -2,2.3646 -2,4.0004 0,2.7614 2.23858,5 5,5 2.7614,0 5,-2.2386 5,-5 0,-1.6358 -0.7856,-3.0882 -2,-4.0004"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path1" />
|
||||
<svg:path
|
||||
d="M 6,12 V 3 h 6 v 9"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path2" />
|
||||
<svg:path
|
||||
d="m 12,3 h 2"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path3" />
|
||||
<svg:path
|
||||
d="m 12,6 h 2"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path4" />
|
||||
<svg:path
|
||||
d="m 12,9 h 2"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path5" />
|
||||
<svg:path
|
||||
d="m 19,7 c 1.1046,0 2,-0.89543 2,-2 0,-1.10457 -0.8954,-2 -2,-2 -1.1046,0 -2,0.89543 -2,2 0,1.10457 0.8954,2 2,2 z"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path6" />
|
||||
<svg:path
|
||||
d="m 9,14 c -1.10457,0 -2,0.8954 -2,2 0,1.1046 0.89543,2 2,2 1.1046,0 2,-0.8954 2,-2 0,-1.1046 -0.8954,-2 -2,-2 z m 0,0 v -3"
|
||||
stroke="#ff6600"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-darkreader-inline-stroke=""
|
||||
id="path7"
|
||||
inkscape:label="path7" />
|
||||
</svg:g>
|
||||
</svg:svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/voc-icon.bmp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
46
assets/voc-icon.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 45 45"
|
||||
fill="currentColor"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="voc-icon.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
width="45"
|
||||
height="45"
|
||||
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="12.529602"
|
||||
inkscape:cx="23.66396"
|
||||
inkscape:cy="30.687327"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1032"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1" />
|
||||
<rect
|
||||
style="fill:#000000;stroke-width:12.8688;stroke-dasharray:51.4753, 25.7377, 12.8688, 25.7377;paint-order:markers stroke fill;fill-opacity:1"
|
||||
id="rect1"
|
||||
width="45"
|
||||
height="45"
|
||||
x="0"
|
||||
y="0" />
|
||||
<path
|
||||
d="M 6.3874472,5.380413 H 14.443723 V 9.4085512 H 6.3874472 Z M 30.556276,35.591449 h 8.056276 v 4.028138 H 30.556276 Z M 2.359309,15.450758 h 10.070345 v 4.028139 H 2.359309 Z m 14.098484,0 H 22.5 v 4.028139 h -6.042207 z m 10.070345,0 h 12.084414 v 4.028139 H 26.528138 Z M 6.3874472,25.521104 H 18.471862 v 4.028138 H 6.3874472 Z m 16.1125528,0 h 6.042207 v 4.028138 H 22.5 Z m 10.070345,0 h 10.070346 v 4.028138 H 32.570345 Z M 18.471862,5.380413 H 42.640691 V 9.4085512 H 18.471862 Z M 2.359309,35.591449 h 24.168829 v 4.028138 H 2.359309 Z"
|
||||
id="path1"
|
||||
style="fill:#cacaca;fill-opacity:1;stroke:none;stroke-width:2.01407;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
429
main.rs
Normal 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();
|
||||
});
|
||||
}
|
||||
205
src/bin/main.rs
@ -1,205 +0,0 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![deny(
|
||||
clippy::mem_forget,
|
||||
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
|
||||
holding buffers for the duration of a data transfer."
|
||||
)]
|
||||
#![allow(unreachable_code)]
|
||||
|
||||
use defmt::info;
|
||||
use embedded_graphics::Drawable;
|
||||
use embedded_graphics::framebuffer::{Framebuffer, buffer_size};
|
||||
use embedded_graphics::image::{Image, ImageRaw};
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::pixelcolor::raw::{BigEndian, LittleEndian};
|
||||
use embedded_graphics::prelude::{Angle, DrawTarget, Point, Primitive, RgbColor, WebColors};
|
||||
use embedded_graphics::primitives::{
|
||||
Circle, PrimitiveStyle, PrimitiveStyleBuilder, Sector, StyledDrawable, Triangle,
|
||||
};
|
||||
use embedded_graphics::text::renderer::TextRenderer;
|
||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||
use esp_hal::clock::CpuClock;
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
use esp_hal::main;
|
||||
use esp_hal::riscv::asm::delay;
|
||||
use esp_hal::rtc_cntl::Rtc;
|
||||
use esp_hal::time::{Duration, Instant, Rate};
|
||||
use esp_hal::timer::Timer;
|
||||
use esp_hal::timer::timg::TimerGroup;
|
||||
use mipidsi::interface::{Interface, SpiInterface};
|
||||
use mipidsi::models::ST7789;
|
||||
use {esp_backtrace as _, esp_println as _};
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
// This creates a default app-descriptor required by the esp-idf bootloader.
|
||||
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
||||
esp_bootloader_esp_idf::esp_app_desc!();
|
||||
|
||||
#[main]
|
||||
fn main() -> ! {
|
||||
// generator version: 1.0.1
|
||||
|
||||
esp_alloc::heap_allocator!(size: 32 * 1024);
|
||||
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||
let peripherals = esp_hal::init(config);
|
||||
let mut timer = Delay::new();
|
||||
|
||||
let spi = esp_hal::spi::master::Spi::new(
|
||||
peripherals.SPI2,
|
||||
esp_hal::spi::master::Config::default()
|
||||
.with_mode(esp_hal::spi::Mode::_0)
|
||||
.with_frequency(Rate::from_mhz(80)),
|
||||
)
|
||||
.unwrap()
|
||||
.with_sck(peripherals.GPIO4)
|
||||
.with_mosi(peripherals.GPIO6);
|
||||
|
||||
let mut cs_output = Output::new(peripherals.GPIO1, Level::High, OutputConfig::default());
|
||||
cs_output.set_high();
|
||||
let spi_device = ExclusiveDevice::new_no_delay(spi, cs_output).unwrap();
|
||||
let _ = Output::new(peripherals.GPIO0, Level::High, OutputConfig::default());
|
||||
|
||||
let mut buffer = [0_u8; 512];
|
||||
|
||||
// Define the display interface with no chip select
|
||||
let di = SpiInterface::new(
|
||||
spi_device,
|
||||
Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default()),
|
||||
&mut buffer,
|
||||
);
|
||||
|
||||
let mut display = mipidsi::Builder::new(ST7789, di)
|
||||
.reset_pin(Output::new(
|
||||
peripherals.GPIO3,
|
||||
Level::Low,
|
||||
OutputConfig::default(),
|
||||
))
|
||||
.invert_colors(mipidsi::options::ColorInversion::Inverted)
|
||||
.init(&mut timer)
|
||||
.unwrap();
|
||||
|
||||
display
|
||||
.set_tearing_effect(mipidsi::options::TearingEffect::Vertical)
|
||||
.unwrap();
|
||||
|
||||
// Create styles used by the drawing operations.
|
||||
let sector_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_color(Rgb565::BLACK)
|
||||
.stroke_width(2)
|
||||
.fill_color(Rgb565::YELLOW)
|
||||
.build();
|
||||
let eye_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_color(Rgb565::BLACK)
|
||||
.stroke_width(1)
|
||||
.fill_color(Rgb565::BLACK)
|
||||
.build();
|
||||
|
||||
//let output_settings = ::new().scale(4).build();
|
||||
//let mut window = Window::new("Pacman", &output_settings);
|
||||
|
||||
// The current progress of the animation
|
||||
const STEPS: i32 = 10;
|
||||
let mut progress: i32 = 0;
|
||||
let delay = Delay::new();
|
||||
|
||||
loop {
|
||||
let mut fb = Framebuffer::<
|
||||
Rgb565,
|
||||
_,
|
||||
LittleEndian,
|
||||
320,
|
||||
240,
|
||||
{ buffer_size::<Rgb565>(320, 240) },
|
||||
>::new();
|
||||
fb.clear(Rgb565::WHITE).unwrap();
|
||||
|
||||
let p = (progress - STEPS).abs();
|
||||
|
||||
// Draw a Sector as the main Pacman feature.
|
||||
let _ = Sector::new(
|
||||
Point::new(2, 2),
|
||||
61,
|
||||
Angle::from_degrees((p * 30 / STEPS) as f32),
|
||||
Angle::from_degrees((360 - 2 * p * 30 / STEPS) as f32),
|
||||
)
|
||||
.draw_styled(§or_style, &mut fb);
|
||||
|
||||
// Draw a Circle as the eye.
|
||||
let _ = Circle::new(Point::new(36, 16), 5).draw_styled(&eye_style, &mut fb);
|
||||
|
||||
delay.delay_millis(50);
|
||||
|
||||
progress = (progress + 1) % (2 * STEPS + 1);
|
||||
|
||||
let img_raw = ImageRaw::<Rgb565, LittleEndian>::new(fb.data(), 320);
|
||||
let image = Image::new(&img_raw, Point::zero());
|
||||
image.draw(&mut display).unwrap();
|
||||
}
|
||||
|
||||
// display
|
||||
// .set_pixels(0, 0, 50, 50, core::iter::repeat(Rgb565::BLUE))
|
||||
// .unwrap();
|
||||
// //display.set_pixel(10, 10, Rgb565::WHITE).unwrap();
|
||||
|
||||
// let i2c = I2c::new(peripherals.I2C0, Config::default())
|
||||
// .unwrap()
|
||||
// .with_sda(peripherals.GPIO8)
|
||||
// .with_scl(peripherals.GPIO9);
|
||||
//
|
||||
// esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 66320);
|
||||
//
|
||||
// let rc = RefCell::new(i2c);
|
||||
//
|
||||
// let mut device = Ens160::new(embedded_hal_bus::i2c::RefCellDevice::new(&rc), 0x53);
|
||||
// timer.delay_millis(500);
|
||||
// device.reset().unwrap();
|
||||
// info!("Reset device");
|
||||
// timer.delay_millis(500);
|
||||
// device.operational().unwrap();
|
||||
// info!("Device operational");
|
||||
//
|
||||
// // Configure the AHT20 temperature and humidity sensor.
|
||||
// let mut aht20_uninit = aht20_driver::AHT20::new(
|
||||
// embedded_hal_bus::i2c::RefCellDevice::new(&rc),
|
||||
// aht20_driver::SENSOR_ADDRESS,
|
||||
// );
|
||||
// let mut aht20 = aht20_uninit.init(&mut timer).unwrap();
|
||||
//
|
||||
// // Take the temperature and humidity measurement.
|
||||
//
|
||||
// loop {
|
||||
// //timer.delay_millis(5000);
|
||||
// if let Ok(status) = device.status() {
|
||||
// if status.data_is_ready() {
|
||||
// let aht20_measurement = aht20.measure(&mut timer).unwrap();
|
||||
// //println!("temperature (aht20): {}C", aht20_measurement.temperature);
|
||||
// //println!("humidity (aht20): {}%", aht20_measurement.humidity);
|
||||
//
|
||||
// let tvoc = device.tvoc().unwrap();
|
||||
// let eco2 = device.eco2().unwrap();
|
||||
// //info!("eco2: {}, tvoc: {}", *eco2, tvoc);
|
||||
// print!(
|
||||
// "{}, {}, {}, {}\n",
|
||||
// *eco2, tvoc, aht20_measurement.temperature, aht20_measurement.humidity
|
||||
// );
|
||||
// // from eCO2
|
||||
// // directly
|
||||
// //let air_quality_index = device.air_quality_index().unwrap();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
loop {
|
||||
info!("Hello world!");
|
||||
let delay_start = Instant::now();
|
||||
while delay_start.elapsed() < Duration::from_millis(500) {}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
fn draw_interface<T: DrawTarget>(target: &mut T) {}
|
||||
13
src/colors.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::prelude::RgbColor;
|
||||
use embedded_graphics::prelude::WebColors;
|
||||
|
||||
pub const BACKGROUND_COLOR: Rgb565 = Rgb565::BLACK;
|
||||
//pub const FRAME_BACKGROUD_COLOR: Rgb565 = Rgb565::new(1, 2, 1);
|
||||
pub const FRAME_BACKGROUD_COLOR: Rgb565 = Rgb565::new(0, 0, 0);
|
||||
pub const FRAME_STROKE_COLOR: Rgb565 = Rgb565::new(4, 9, 4);
|
||||
|
||||
pub const MAIN_TEXT_COLOR: Rgb565 = Rgb565::WHITE;
|
||||
pub const SUB_TEXT_COLOR: Rgb565 = Rgb565::CSS_DARK_GRAY;
|
||||
|
||||
pub const FRAME_STROKE: u32 = 1;
|
||||
64
src/display.rs
Normal 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
|
||||
}
|
||||
352
src/graph.rs
Normal file
@ -0,0 +1,352 @@
|
||||
use embedded_graphics::Pixel;
|
||||
use embedded_graphics::image::GetPixel;
|
||||
use embedded_graphics::mono_font::MonoTextStyle;
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::prelude::DrawTarget;
|
||||
use embedded_graphics::prelude::Drawable;
|
||||
use embedded_graphics::prelude::Point;
|
||||
use embedded_graphics::prelude::Primitive;
|
||||
use embedded_graphics::prelude::RgbColor;
|
||||
use embedded_graphics::prelude::Size;
|
||||
use embedded_graphics::prelude::WebColors;
|
||||
use embedded_graphics::primitives::Line;
|
||||
use embedded_graphics::primitives::PrimitiveStyle;
|
||||
use embedded_graphics::primitives::line::StyledPixelsIterator;
|
||||
use embedded_graphics::text::Text;
|
||||
use fixed::traits::Fixed;
|
||||
use fixed::types::I16F16;
|
||||
use fixed_macro::fixed;
|
||||
use heapless::format;
|
||||
use profont::PROFONT_10_POINT;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
#[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(
|
||||
(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: FixedType, colors: &[Rgb565]) -> Rgb565 {
|
||||
if x == FixedType::from_num(1.) {
|
||||
return colors[colors.len() - 1];
|
||||
}
|
||||
|
||||
if x == FixedType::from_num(0.) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
x *= FixedType::from_num(colors.len() - 1);
|
||||
|
||||
let index = x.floor();
|
||||
let interp = x - index;
|
||||
|
||||
rgb565_interpolate(
|
||||
colors[index.to_num::<usize>()],
|
||||
colors[index.to_num::<usize>() + 1],
|
||||
interp.to_num(),
|
||||
)
|
||||
}
|
||||
|
||||
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 size = Size::new(
|
||||
target.bounding_box().size.width,
|
||||
target.bounding_box().size.height,
|
||||
);
|
||||
|
||||
// Draw data as WHITE line
|
||||
let mut start = Point::new(
|
||||
0,
|
||||
map_float(
|
||||
data.clone().next().unwrap(),
|
||||
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.enumerate().skip(1) {
|
||||
let point = Point::new(
|
||||
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))
|
||||
.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(
|
||||
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 - 2 * y as i32) % 7 == 0 {
|
||||
let _ = Pixel(
|
||||
position,
|
||||
rgb565_interpolate(
|
||||
height_color,
|
||||
Rgb565::BLACK,
|
||||
FixedType::from_num(1) - FixedType::from_num(height_factor),
|
||||
),
|
||||
)
|
||||
.draw(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn min_indicator<
|
||||
// I: Iterator<Item = f32> + Clone,
|
||||
// T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>,
|
||||
// >(
|
||||
// data: I,
|
||||
// data_count: usize,
|
||||
// target: &mut T,
|
||||
// ) {
|
||||
// let size = target.bounding_box().size;
|
||||
// let (min_index, min) = data
|
||||
// .clone()
|
||||
// .enumerate()
|
||||
// .reduce(|a, b| if a.1 < b.1 { a } else { b })
|
||||
// .unwrap_or((0, 0.));
|
||||
//
|
||||
// let min_x = map_float(
|
||||
// min_index as f32,
|
||||
// 0.,
|
||||
// data_count as f32,
|
||||
// 0.,
|
||||
// size.width as f32,
|
||||
// ) as i32;
|
||||
//
|
||||
// // let _ = Line::new(Point::new(min_x, 0), Point::new(min_x, size.height as i32))
|
||||
// // .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1))
|
||||
// // .draw(target);
|
||||
// for y in 0..size.height {
|
||||
// if (y / 2) % 2 == 0 {
|
||||
// let position = Point::new(min_x, y as i32);
|
||||
// let _ = Pixel(
|
||||
// position,
|
||||
// rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6),
|
||||
// )
|
||||
// .draw(target);
|
||||
//
|
||||
// // let position = Point::new(min_x + 1, y as i32);
|
||||
// // let _ = Pixel(
|
||||
// // position,
|
||||
// // rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6),
|
||||
// // )
|
||||
// // .draw(target);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let minimum_text = "Minimum";
|
||||
// let font = &PROFONT_10_POINT;
|
||||
//
|
||||
// let text_start = if min_x < (size.width / 2) as i32 {
|
||||
// 5
|
||||
// } else {
|
||||
// -((minimum_text.len() + 1) as i32 * font.character_size.width as i32 + 3)
|
||||
// };
|
||||
//
|
||||
// let style = MonoTextStyle::new(
|
||||
// &PROFONT_10_POINT,
|
||||
// rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8),
|
||||
// );
|
||||
// let _ = Text::new(
|
||||
// minimum_text,
|
||||
// Point::new(min_x + text_start as i32, 10),
|
||||
// style,
|
||||
// )
|
||||
// .draw(target);
|
||||
//
|
||||
// let value = format!(16; "{:.1}", min).unwrap();
|
||||
// let _ = Text::new(
|
||||
// value.as_str(),
|
||||
// Point::new(
|
||||
// min_x + text_start as i32,
|
||||
// 10 + font.character_size.height as i32,
|
||||
// ),
|
||||
// style,
|
||||
// )
|
||||
// .draw(target);
|
||||
// }
|
||||
//
|
||||
// pub fn max_indicator<
|
||||
// T: DrawTarget<Color = Rgb565> + GetPixel<Color = Rgb565>,
|
||||
// I: Iterator<Item = f32> + Clone,
|
||||
// >(
|
||||
// data: I,
|
||||
// data_count: usize,
|
||||
// target: &mut T,
|
||||
// ) {
|
||||
// let size = target.bounding_box().size;
|
||||
// let (max_index, max) = data
|
||||
// .clone()
|
||||
// .enumerate()
|
||||
// .reduce(|a, b| if a.1 > b.1 { a } else { b })
|
||||
// .unwrap_or((0, 0.));
|
||||
//
|
||||
// let max_x = map_float(
|
||||
// max_index as f32,
|
||||
// 0.,
|
||||
// data_count as f32,
|
||||
// 0.,
|
||||
// size.width as f32,
|
||||
// ) as i32;
|
||||
//
|
||||
// for y in 0..size.height {
|
||||
// if (y / 2) % 2 == 0 {
|
||||
// let position = Point::new(max_x, y as i32);
|
||||
// let _ = Pixel(
|
||||
// position,
|
||||
// rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6),
|
||||
// )
|
||||
// .draw(target);
|
||||
//
|
||||
// // let position = Point::new(max_x + 1, y as i32);
|
||||
// // let _ = Pixel(
|
||||
// // position,
|
||||
// // rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6),
|
||||
// // )
|
||||
// // .draw(target);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let maximum_text = "Maximum";
|
||||
// let font = &PROFONT_10_POINT;
|
||||
//
|
||||
// let text_start = if max_x < (size.width / 2) as i32 {
|
||||
// 5
|
||||
// } else {
|
||||
// -((maximum_text.len() + 1) as i32 * font.character_size.width as i32 + 3)
|
||||
// };
|
||||
//
|
||||
// let style = MonoTextStyle::new(
|
||||
// &PROFONT_10_POINT,
|
||||
// rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8),
|
||||
// );
|
||||
// let _ = Text::new(
|
||||
// maximum_text,
|
||||
// Point::new(
|
||||
// max_x + text_start as i32,
|
||||
// size.height as i32 - font.character_size.height as i32 * 2,
|
||||
// ),
|
||||
// style,
|
||||
// )
|
||||
// .draw(target);
|
||||
//
|
||||
// let value = format!(16; "{:.1}", max).unwrap();
|
||||
// let _ = Text::new(
|
||||
// value.as_str(),
|
||||
// Point::new(
|
||||
// max_x + text_start as i32,
|
||||
// size.height as i32 - font.character_size.height as i32,
|
||||
// ),
|
||||
// style,
|
||||
// )
|
||||
// .draw(target);
|
||||
// }
|
||||
55
src/images.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use core::cell::UnsafeCell;
|
||||
use core::include_bytes;
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use critical_section::Mutex;
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use tinybmp::Bmp;
|
||||
|
||||
pub type StaticImage = Mutex<UnsafeCell<MaybeUninit<Bmp<'static, Rgb565>>>>;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub static HUMIDITY_ICON: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
pub static TEMPERATURE_ICON: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
pub static VOC_ICON: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
pub static CO2_ICON: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
|
||||
// Tendency indicators
|
||||
pub static TENDENCY_RISING: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
pub static TENDENCY_STEADY: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
pub static TENDENCY_FALLING: StaticImage = Mutex::new(UnsafeCell::new(MaybeUninit::zeroed()));
|
||||
|
||||
macro_rules! load_image {
|
||||
($stat:expr, $path:expr) => {
|
||||
critical_section::with(|cs| {
|
||||
*$stat.borrow(cs).get() =
|
||||
MaybeUninit::new(Bmp::from_slice(include_bytes!($path)).unwrap());
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_image {
|
||||
($image:expr) => {
|
||||
unsafe {
|
||||
use core::mem::MaybeUninit;
|
||||
use tinybmp::Bmp;
|
||||
&*core::mem::transmute::<*mut MaybeUninit<Bmp<Rgb565>>, *mut Bmp<Rgb565>>(
|
||||
critical_section::with(|cs| $image.borrow(cs).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");
|
||||
|
||||
load_image!(TENDENCY_RISING, "../assets/indic-rising.bmp");
|
||||
load_image!(TENDENCY_STEADY, "../assets/indic-steady.bmp");
|
||||
load_image!(TENDENCY_FALLING, "../assets/indic-falling.bmp");
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
#![no_std]
|
||||
410
src/main.rs
Normal file
@ -0,0 +1,410 @@
|
||||
#![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."
|
||||
)]
|
||||
|
||||
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;
|
||||
|
||||
esp_bootloader_esp_idf::esp_app_desc!();
|
||||
use esp_backtrace as _;
|
||||
use esp_println as _;
|
||||
use heapless::HistoryBuf;
|
||||
use heapless::format;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use crate::display::MainDisplay;
|
||||
use crate::images::StaticImage;
|
||||
use crate::sampler::History;
|
||||
use crate::sampler::Sample;
|
||||
use crate::sampler::Sampler;
|
||||
|
||||
pub enum ApplicationEvent {
|
||||
ButtonPress,
|
||||
LongButtonPress,
|
||||
NewSample(Sample),
|
||||
}
|
||||
|
||||
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]
|
||||
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 timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||
esp_rtos::start(timg0.timer0, software_interrupt.software_interrupt0);
|
||||
|
||||
info!("Init done.");
|
||||
info!("Setting up display");
|
||||
let mut timer = esp_hal::delay::Delay::new();
|
||||
let mut display = display::setup_display(
|
||||
peripherals.SPI2,
|
||||
peripherals.GPIO4,
|
||||
peripherals.GPIO6,
|
||||
peripherals.GPIO0,
|
||||
peripherals.GPIO1,
|
||||
&mut timer,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Setup button
|
||||
let btn = Input::new(
|
||||
peripherals.GPIO10,
|
||||
InputConfig::default().with_pull(Pull::Down),
|
||||
);
|
||||
|
||||
// Setup sampler
|
||||
let sampler = SAMPLER.init(Sampler::new(
|
||||
peripherals.I2C0,
|
||||
peripherals.GPIO8,
|
||||
peripherals.GPIO9,
|
||||
&mut timer,
|
||||
));
|
||||
sampler.sample(&mut timer);
|
||||
Timer::after_secs(2).await;
|
||||
sampler.sample(&mut timer);
|
||||
|
||||
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,
|
||||
eco2: Tendency,
|
||||
tvoc: Tendency,
|
||||
}
|
||||
|
||||
impl Tendencies {
|
||||
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 {
|
||||
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 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/sampler.rs
Normal file
@ -0,0 +1,203 @@
|
||||
use core::cell::RefCell;
|
||||
use core::ops::Add;
|
||||
use core::ops::Mul;
|
||||
use core::ops::Sub;
|
||||
|
||||
use aht20_driver::AHT20;
|
||||
use alloc::rc::Rc;
|
||||
use core::default::Default;
|
||||
use embedded_hal_bus::i2c::RcDevice;
|
||||
//use ens160_aq::Ens160;
|
||||
use ens160::Ens160;
|
||||
use esp_hal::Blocking;
|
||||
use esp_hal::delay::Delay;
|
||||
use esp_hal::gpio::interconnect::PeripheralOutput;
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::i2c::master::Instance;
|
||||
use esp_hal::time::Duration;
|
||||
use esp_hal::time::Instant;
|
||||
use heapless::HistoryBuf;
|
||||
|
||||
pub struct Sampler<'a> {
|
||||
ens160: Ens160<RcDevice<I2c<'a, Blocking>>>,
|
||||
aht20: aht20_driver::AHT20Initialized<RcDevice<I2c<'a, Blocking>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Sample {
|
||||
pub temperature: f32,
|
||||
pub humidity: f32,
|
||||
pub eco2: f32,
|
||||
pub tvoc: f32,
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn zero() -> Self {
|
||||
Sample {
|
||||
temperature: 0.,
|
||||
humidity: 0.,
|
||||
eco2: 0.,
|
||||
tvoc: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Sample> for Sample {
|
||||
type Output = Sample;
|
||||
|
||||
fn add(self, rhs: Sample) -> Self::Output {
|
||||
Sample {
|
||||
temperature: self.temperature + rhs.temperature,
|
||||
humidity: self.humidity + rhs.humidity,
|
||||
eco2: self.eco2 + rhs.eco2,
|
||||
tvoc: self.tvoc + rhs.tvoc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Sample> for Sample {
|
||||
type Output = Sample;
|
||||
|
||||
fn sub(self, rhs: Sample) -> Self::Output {
|
||||
Sample {
|
||||
temperature: self.temperature - rhs.temperature,
|
||||
humidity: self.humidity - rhs.humidity,
|
||||
eco2: self.eco2 - rhs.eco2,
|
||||
tvoc: self.tvoc - rhs.tvoc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Sample> for Sample {
|
||||
type Output = Sample;
|
||||
|
||||
fn mul(self, rhs: Sample) -> Self::Output {
|
||||
Sample {
|
||||
temperature: self.temperature * rhs.temperature,
|
||||
humidity: self.humidity * rhs.humidity,
|
||||
eco2: self.eco2 * rhs.eco2,
|
||||
tvoc: self.tvoc * rhs.tvoc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f32> for Sample {
|
||||
type Output = Sample;
|
||||
|
||||
fn mul(self, rhs: f32) -> Self::Output {
|
||||
Sample {
|
||||
temperature: self.temperature * rhs,
|
||||
humidity: self.humidity * rhs,
|
||||
eco2: self.eco2 * rhs,
|
||||
tvoc: self.tvoc * rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sampler<'a> {
|
||||
pub fn new(
|
||||
i2c: impl Instance + 'a,
|
||||
sda: impl PeripheralOutput<'a>,
|
||||
scl: impl PeripheralOutput<'a>,
|
||||
timer: &mut Delay,
|
||||
) -> Self {
|
||||
let i2c = I2c::new(i2c, Default::default())
|
||||
.unwrap()
|
||||
.with_sda(sda)
|
||||
.with_scl(scl);
|
||||
|
||||
let i2c = Rc::new(RefCell::new(i2c));
|
||||
|
||||
let mut ens160 = Ens160::new(embedded_hal_bus::i2c::RcDevice::new(i2c.clone()), 0x53);
|
||||
timer.delay_millis(500);
|
||||
ens160.reset().unwrap();
|
||||
timer.delay_millis(500);
|
||||
ens160.operational().unwrap();
|
||||
|
||||
let aht20_uninit = AHT20::new(
|
||||
embedded_hal_bus::i2c::RcDevice::new(i2c.clone()),
|
||||
aht20_driver::SENSOR_ADDRESS,
|
||||
);
|
||||
|
||||
let aht20 = aht20_uninit.init(timer).unwrap();
|
||||
|
||||
Sampler { ens160, aht20 }
|
||||
}
|
||||
|
||||
pub fn sample(&mut self, timer: &mut Delay) -> Sample {
|
||||
let aht20_measurement = self.aht20.measure(timer).unwrap();
|
||||
|
||||
Sample {
|
||||
temperature: aht20_measurement.temperature,
|
||||
humidity: aht20_measurement.humidity,
|
||||
eco2: *self.ens160.eco2().unwrap() as f32,
|
||||
tvoc: self.ens160.tvoc().unwrap() as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const SECONDS_PER_SAMPLES: usize = 1;
|
||||
pub const MIN_5_LENGTH: usize = (5 * 60) / SECONDS_PER_SAMPLES;
|
||||
|
||||
pub struct History {
|
||||
// 5 minutes, every 5 seconds
|
||||
pub min5: heapless::history_buf::HistoryBuf<Sample, MIN_5_LENGTH>,
|
||||
|
||||
// 2 hours every 5 seconds
|
||||
pub hour2: heapless::history_buf::HistoryBuf<Sample, { (2 * 60) / SECONDS_PER_SAMPLES }>,
|
||||
|
||||
// 24 hours every 5 minutes
|
||||
pub day: heapless::history_buf::HistoryBuf<Sample, { (24 * 60) / 5 }>,
|
||||
|
||||
samples_since_day: u32,
|
||||
|
||||
last_sample: Instant,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new(sampler: &mut Sampler, timer: &mut Delay) -> Self {
|
||||
let mut min5 = HistoryBuf::new();
|
||||
let mut hour2 = HistoryBuf::new();
|
||||
let mut day = HistoryBuf::new();
|
||||
|
||||
// First sampler
|
||||
let sample = sampler.sample(timer);
|
||||
min5.write(sample);
|
||||
hour2.write(sample);
|
||||
day.write(sample);
|
||||
|
||||
History {
|
||||
min5,
|
||||
hour2,
|
||||
day,
|
||||
samples_since_day: 0,
|
||||
|
||||
last_sample: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, sampler: &mut Sampler, timer: &mut Delay) -> bool {
|
||||
let now = Instant::now();
|
||||
|
||||
if now - self.last_sample > Duration::from_secs(SECONDS_PER_SAMPLES as u64) {
|
||||
let sample = sampler.sample(timer);
|
||||
self.last_sample = Instant::now();
|
||||
self.samples_since_day += 1;
|
||||
|
||||
if self.samples_since_day as usize == MIN_5_LENGTH {
|
||||
// Compute average
|
||||
let avg = self.min5.iter().fold(Sample::zero(), |a, b| a + *b)
|
||||
* (1. / self.min5.len() as f32);
|
||||
self.day.write(avg);
|
||||
|
||||
self.samples_since_day = 0;
|
||||
}
|
||||
|
||||
self.min5.write(sample);
|
||||
self.hour2.write(sample);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/views.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod detail;
|
||||
pub mod icon;
|
||||
pub mod menu;
|
||||
147
src/views/detail.rs
Normal file
@ -0,0 +1,147 @@
|
||||
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::MeasurementType;
|
||||
use crate::Tendencies;
|
||||
use crate::colors::FRAME_STROKE_COLOR;
|
||||
use crate::colors::MAIN_TEXT_COLOR;
|
||||
use crate::colors::SUB_TEXT_COLOR;
|
||||
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<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(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)
|
||||
.flex_frame()
|
||||
.with_infinite_max_height()
|
||||
.with_vertical_alignment(VerticalAlignment::Bottom)
|
||||
.with_max_height(25),
|
||||
Spacer::default(),
|
||||
//Spacer::default().flex_frame().with_max_width(10),
|
||||
)),
|
||||
// Window
|
||||
Spacer::default()
|
||||
.flex_frame()
|
||||
.with_infinite_max_width()
|
||||
.with_infinite_max_height(),
|
||||
))
|
||||
.with_alignment(HorizontalAlignment::Leading)
|
||||
}
|
||||
21
src/views/icon.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use buoyant::view::View;
|
||||
use buoyant::view::ViewExt;
|
||||
use buoyant::view::ZStack;
|
||||
use buoyant::view::shape::Rectangle;
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
|
||||
use crate::colors::BACKGROUND_COLOR;
|
||||
use crate::get_image;
|
||||
use crate::images::StaticImage;
|
||||
|
||||
pub fn icon_box_view(box_color: Rgb565, icon: &'static StaticImage) -> impl View<Rgb565> {
|
||||
ZStack::new((
|
||||
Rectangle
|
||||
.corner_radius(10)
|
||||
.foreground_color(BACKGROUND_COLOR),
|
||||
buoyant::view::Image::new(get_image!(icon)),
|
||||
))
|
||||
.flex_frame()
|
||||
.with_max_size(53, 53)
|
||||
.with_min_size(53, 53)
|
||||
}
|
||||
91
src/views/menu.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use buoyant::view::VStack;
|
||||
use buoyant::view::View;
|
||||
|
||||
use buoyant::view::prelude::*;
|
||||
use embedded_graphics::pixelcolor::Rgb565;
|
||||
use embedded_graphics::prelude::*;
|
||||
use profont::PROFONT_18_POINT;
|
||||
use profont::PROFONT_24_POINT;
|
||||
|
||||
use crate::MeasurementType;
|
||||
use crate::Tendencies;
|
||||
use crate::Tendency;
|
||||
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;
|
||||
use crate::get_image;
|
||||
use crate::sampler::Sample;
|
||||
use crate::views::icon::icon_box_view;
|
||||
|
||||
pub fn menu_view(sample: Sample, tendencies: Tendencies) -> impl View<Rgb565> {
|
||||
VStack::new((
|
||||
HStack::new((
|
||||
main_menu_indicator(MeasurementType::Temperature, tendencies, sample),
|
||||
main_menu_indicator(MeasurementType::Humidity, tendencies, sample),
|
||||
))
|
||||
.with_spacing(2),
|
||||
HStack::new((
|
||||
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: MeasurementType,
|
||||
tendencies: Tendencies,
|
||||
sample: Sample,
|
||||
) -> impl View<Rgb565> {
|
||||
Rectangle
|
||||
.corner_radius(5)
|
||||
.stroked(FRAME_STROKE)
|
||||
.foreground_color(FRAME_STROKE_COLOR)
|
||||
.background(Alignment::Center, || {
|
||||
ZStack::new((
|
||||
Rectangle
|
||||
.corner_radius(15)
|
||||
.foreground_color(FRAME_BACKGROUD_COLOR),
|
||||
VStack::new((
|
||||
HStack::new((
|
||||
Spacer::default(),
|
||||
icon_box_view(FRAME_STROKE_COLOR, indicator_type.get_corresponding_icon()),
|
||||
Spacer::default(),
|
||||
)),
|
||||
HStack::new((
|
||||
Spacer::default(),
|
||||
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(),
|
||||
&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(),
|
||||
)),
|
||||
))
|
||||
.with_alignment(HorizontalAlignment::Center)
|
||||
.flex_frame(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub 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)
|
||||
}
|
||||