From b0250e08bf7ad7632b16a07f2754d0cde3b92e5f Mon Sep 17 00:00:00 2001 From: Matthias Schild Date: Tue, 25 Aug 2026 00:00:07 +0200 Subject: [PATCH] feat: dynamic count of machines, use config --- .gitignore | 1 + Cargo.lock | 312 +++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + src/config.rs | 35 ++++++ src/data.rs | 44 +++---- src/main.rs | 13 ++- src/tui.rs | 134 ++++++++++++++-------- 7 files changed, 438 insertions(+), 102 deletions(-) create mode 100644 src/config.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..db38427 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index df2f6e9..84c7f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "atomic" version = "0.6.1" @@ -102,6 +119,9 @@ name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -147,9 +167,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -203,6 +223,55 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "config" +version = "0.15.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -280,6 +349,12 @@ dependencies = [ "winapi", ] +[[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" @@ -302,9 +377,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -312,26 +387,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -364,7 +439,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -381,6 +456,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.12" @@ -398,9 +482,18 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] [[package]] name = "equivalent" @@ -408,6 +501,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -557,7 +661,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -634,6 +738,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -656,6 +766,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -693,7 +812,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -732,6 +851,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "kasuari" version = "0.4.12" @@ -799,9 +929,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" @@ -827,6 +957,7 @@ name = "machinetui" version = "0.1.0" dependencies = [ "anyhow", + "config", "crossterm", "futures", "rand 0.10.2", @@ -956,6 +1087,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "palette" version = "0.7.7" @@ -1012,6 +1153,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pest" version = "2.9.0" @@ -1345,6 +1492,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rumqttc" version = "0.25.1" @@ -1366,6 +1527,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1398,7 +1569,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.14", + "rustls-webpki 0.103.15", "subtle", "zeroize", ] @@ -1446,9 +1617,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -1522,6 +1693,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -1539,7 +1722,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1555,6 +1738,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1703,9 +1895,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -1825,7 +2017,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1860,6 +2052,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tokio" version = "1.53.1" @@ -1885,7 +2086,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1923,6 +2124,43 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" @@ -1978,9 +2216,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "atomic", "getrandom 0.4.3", @@ -2245,12 +2483,32 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "yaml-rust2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36710ce3a279cfce8465dbab826f161675a262950b922cb2c3663852dfe9eb0" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "zeroize" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index da24fba..b73bfde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] anyhow = "1.0.104" +config = "0.15.25" crossterm = { version = "0.29.0", features = ["event-stream"] } futures = "0.3.34" rand = "0.10.2" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1d2844f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use rumqttc::MqttOptions; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct ConfigData { + pub mqtt: MqttConfig, + #[serde(rename = "machine", default)] + pub machines: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MqttConfig { + pub device_id: String, + pub host: String, + pub port: u16, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MachineConfig { + pub name: String, + pub topic: String, +} + +pub fn load() -> Result { + let source = config::File::with_name("config").format(config::FileFormat::Toml); + let config = config::Config::builder().add_source(source).build()?; + Ok(config.try_deserialize()?) +} + +impl MqttConfig { + pub fn as_options(&self) -> MqttOptions { + MqttOptions::new(&self.device_id, &self.host, self.port) + } +} diff --git a/src/data.rs b/src/data.rs index 7b36c72..7b5bbf1 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,11 +1,11 @@ use std::time::Duration; use anyhow::Result; -use rumqttc::{AsyncClient, Event, MqttOptions, Packet, Publish, QoS}; +use rumqttc::{AsyncClient, Event, Packet, Publish, QoS}; use serde::Deserialize; use time::{OffsetDateTime, PrimitiveDateTime}; -use crate::{data, events}; +use crate::{config::ConfigData, data, events}; time::serde::format_description!( tasmota_time, @@ -13,11 +13,8 @@ time::serde::format_description!( "[year]-[month]-[day]T[hour]:[minute]:[second]" ); -const MACHINE1_TOPIC: &str = "tele/tasmota_F68B94/SENSOR"; -const MACHINE2_TOPIC: &str = "tele/tasmota_F81A3C/SENSOR"; - pub struct MachineData { - pub name: String, + pub topic: String, pub voltage: f32, pub current: f32, pub last_updated: OffsetDateTime, @@ -40,12 +37,12 @@ pub struct TasmotaEnergyData { current: f32, } -fn publish_to_machine_data(name: String, publish: Publish) -> Result { +fn publish_to_machine_data(topic: String, publish: Publish) -> Result { let data = publish.payload.to_vec(); let data: TasmotaData = serde_json::from_slice(&data)?; Ok(data::MachineData { - name, + topic, voltage: data.energy.voltage, current: data.energy.current, last_updated: data.time.assume_utc(), @@ -65,36 +62,31 @@ fn start_timestamp_task(event_sender: events::EventSender) { }); } -async fn start_data_fetch_task(event_sender: events::EventSender) -> Result<()> { - let options = MqttOptions::new("machinetui", "192.168.178.251", 1883); +async fn start_data_fetch_task( + config: &ConfigData, + event_sender: events::EventSender, +) -> Result<()> { + let options = config.mqtt.as_options(); let (client, mut event_loop) = AsyncClient::new(options, 10); - client.subscribe(MACHINE1_TOPIC, QoS::AtLeastOnce).await?; - client.subscribe(MACHINE2_TOPIC, QoS::AtLeastOnce).await?; + for machine in &config.machines { + client.subscribe(&machine.topic, QoS::AtLeastOnce).await?; + } tokio::spawn(async move { + // Keep client alive let _client = client; loop { match event_loop.poll().await { Ok(Event::Incoming(Packet::Publish(publish))) => match publish.topic.as_str() { - MACHINE1_TOPIC => { - let machine_data = - publish_to_machine_data("machine 1".into(), publish).unwrap(); + topic => { + let machine_data = publish_to_machine_data(topic.into(), publish).unwrap(); event_sender .send(events::Event::MachineUpdate(machine_data)) .await .unwrap(); } - MACHINE2_TOPIC => { - let machine_data = - publish_to_machine_data("machine 2".into(), publish).unwrap(); - event_sender - .send(events::Event::MachineUpdate(machine_data)) - .await - .unwrap(); - } - _ => {} }, Ok(_) => {} Err(_err) => {} @@ -105,8 +97,8 @@ async fn start_data_fetch_task(event_sender: events::EventSender) -> Result<()> Ok(()) } -pub async fn start(event_sender: events::EventSender) -> Result<()> { +pub async fn start(config: &ConfigData, event_sender: events::EventSender) -> Result<()> { start_timestamp_task(event_sender.clone()); - start_data_fetch_task(event_sender).await?; + start_data_fetch_task(config, event_sender).await?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 05e6cee..d042ab7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,21 @@ -use anyhow::Result; - +mod config; mod data; mod events; mod interaction; mod tui; +use anyhow::Result; + #[tokio::main] async fn main() -> Result<()> { + let config = config::load()?; let (event_sender, event_receiver) = events::channel(); - data::start(event_sender.clone()).await?; - tokio::try_join!(tui::start(event_receiver), interaction::start(event_sender))?; + data::start(&config, event_sender.clone()).await?; + tokio::try_join!( + tui::start(&config, event_receiver), + interaction::start(event_sender) + )?; Ok(()) } diff --git a/src/tui.rs b/src/tui.rs index 9d2cc89..6bf6a4b 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,71 +1,96 @@ +use std::collections::{HashMap, VecDeque}; + use anyhow::Result; use ratatui::{ prelude::*, symbols::border, - widgets::{Block, Padding, Paragraph}, + widgets::{Axis, Block, Chart, Dataset, GraphType, Padding, Paragraph}, }; use time::{OffsetDateTime, format_description::BorrowedFormatItem, macros::format_description}; -use crate::{data, events}; +use crate::{ + config::ConfigData, + data::{self}, + events, +}; const TIME_FORMAT: &'static [BorrowedFormatItem] = format_description!("[hour]:[minute]:[second]"); +const HISTORY_LEN: usize = 120; -pub struct App { +pub struct App<'a> { pub timestamp: OffsetDateTime, - pub machines: Vec, + pub machines: HashMap, + pub history: HashMap>, + + config: &'a ConfigData, } -impl App { - pub fn new() -> Self { +impl<'a> App<'a> { + pub fn new(config: &'a ConfigData) -> Self { Self { timestamp: OffsetDateTime::now_local().unwrap(), - machines: vec![ - data::MachineData { - name: "machine 1".into(), - voltage: 0f32, - current: 0f32, - last_updated: OffsetDateTime::now_utc(), - }, - data::MachineData { - name: "machine 2".into(), - voltage: 0f32, - current: 0f32, - last_updated: OffsetDateTime::now_utc(), - }, - ], + machines: HashMap::new(), + history: HashMap::new(), + config, } } } -impl Widget for &App { +impl<'a> Widget for &App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { - let [headline_area, machines_area, _] = Layout::vertical([ + let [headline_area, machines_area, chart_area] = Layout::vertical([ Constraint::Length(3), Constraint::Length(6), Constraint::Min(0), ]) .areas(area); - let [machine1_area, machine2_area] = - Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(machines_area); - Headline { timestamp: &self.timestamp, } .render(headline_area, buf); - MachineData { - data: &self.machines[0], - } - .render(machine1_area, buf); + let machine_area_const = vec![Constraint::Fill(1); self.config.machines.len()]; + let machine_areas = Layout::horizontal(machine_area_const).split(machines_area); + let machine_zip = self + .config + .machines + .iter() + .zip(machine_areas.iter().copied()); - MachineData { - data: &self.machines[1], + for (machine_config, machine_area) in machine_zip { + let machine_data = self.machines.get(&machine_config.topic); + + MachineData { + name: &machine_config.name, + data: machine_data, + } + .render(machine_area, buf); } - .render(machine2_area, buf); + + let datasets: Vec> = self + .history + .iter() + .map(|(name, data)| { + let data = data.as_slices().0; + Dataset::default() + .name(Line::from(name.as_str())) + .marker(symbols::Marker::Braille) + .graph_type(GraphType::Line) + .style(Color::Cyan) + .data(data) + }) + .collect(); + + let chart = Chart::new(datasets) + .block(Block::bordered().title("Current usage (A)")) + .x_axis(Axis::default().bounds([0.0, HISTORY_LEN as f64])) + .y_axis(Axis::default().bounds([0.0, 10f64])); + + chart.render(chart_area, buf); } } @@ -102,7 +127,8 @@ impl<'a> Widget for &Headline<'a> { } pub struct MachineData<'a> { - data: &'a data::MachineData, + name: &'a str, + data: Option<&'a data::MachineData>, } impl<'a> Widget for &MachineData<'a> { @@ -117,6 +143,20 @@ impl<'a> Widget for &MachineData<'a> { let block_area = block.inner(area); block.render(area, buf); + let machine = match self.data { + Some(data) => data, + None => { + // Render no data + let area_const = [Constraint::Length(1), Constraint::Min(0)]; + let [name_area, no_data_area] = Layout::vertical(area_const).areas(block_area); + + let text = format!("Name: {}", self.name).bold().fg(Color::Blue); + Paragraph::new(text).render(name_area, buf); + Paragraph::new("no data").render(no_data_area, buf); + return; + } + }; + let layout_const = [ Constraint::Length(1), Constraint::Length(1), @@ -127,18 +167,18 @@ impl<'a> Widget for &MachineData<'a> { Layout::vertical(layout_const).areas(block_area); let format = format_description!("[year]-[month]-[day] [hour]:[month]:[second]"); - let last_updated = self.data.last_updated.format(format).unwrap(); + let last_updated = machine.last_updated.format(format).unwrap(); - let text = format!("Name: {}", self.data.name).bold().fg(Color::Blue); + let text = format!("Name: {}", self.name).bold().fg(Color::Blue); Paragraph::new(text).render(name_area, buf); let text = Line::from(vec![ "Voltage: ".bold(), - format!("{:.0} V", self.data.voltage).into(), + format!("{:.0} V", machine.voltage).into(), ]); Paragraph::new(text).render(voltage_area, buf); let text = Line::from(vec![ "Current: ".bold(), - format!("{:.03} A", self.data.current).into(), + format!("{:.03} A", machine.current).into(), ]); Paragraph::new(text).render(current_area, buf); let text = Line::from(vec!["Last updated: ".bold(), last_updated.into()]); @@ -146,12 +186,12 @@ impl<'a> Widget for &MachineData<'a> { } } -pub async fn start(event_receiver: events::EventReceiver) -> Result<()> { +pub async fn start(config: &ConfigData, event_receiver: events::EventReceiver) -> Result<()> { let mut event_receiver = event_receiver; let mut terminal = ratatui::init(); let mut running = true; - let mut app = App::new(); + let mut app = App::new(config); while running { // render tui @@ -163,12 +203,16 @@ pub async fn start(event_receiver: events::EventReceiver) -> Result<()> { match event_receiver.recv().await { Some(events::Event::Timestamp(t)) => app.timestamp = t, Some(events::Event::MachineUpdate(data)) => { - for machine in &mut app.machines { - if machine.name == data.name { - machine.voltage = data.voltage; - machine.current = data.current; - machine.last_updated = data.last_updated; - } + let name = data.topic.clone(); + let current = data.current; + + app.machines.insert(name.clone(), data); + + let history = app.history.entry(name).or_default(); + let x = history.len() as f64; + history.push_back((x, current as f64)); + if history.len() > HISTORY_LEN { + history.pop_front(); } } Some(events::Event::Exit) => running = false,