From 30e7449034b2910e6c2cc9a2f7611719564a82ec Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:46:04 +0200 Subject: [PATCH] Feat/sysinfo (#30) * Add specific feature for input hotkeys * Added linux sysinfo stats module * Add config options for stat bar max fills * Use sysinfo crate instead of apex-sysinfo bindings --------- Co-authored-by: Andy Brennan --- .vscode/settings.json | 3 + Cargo.toml | 7 +- settings.toml | 17 ++++ src/main.rs | 6 +- src/providers/mod.rs | 2 + src/providers/sysinfo.rs | 206 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/providers/sysinfo.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e6215ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": ["default", "sysinfo"] +} diff --git a/Cargo.toml b/Cargo.toml index 82e8dd4..1a85a62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ apex-input = {path = "./apex-input" } apex-music = { path = "./apex-music" } apex-simulator = { path = "./apex-simulator", optional = true } apex-engine = { path = "./apex-engine", optional = true } +sysinfo = { version = "0.27.7", optional = true } lazy_static = "1.4.0" @@ -69,8 +70,8 @@ dbus-support = ["dbus", "dbus-tokio", "apex-mpris2"] http = ["serde", "serde_json", "reqwest"] crypto = ["http"] simulator = ["apex-simulator"] -usb = ["apex-hardware/usb", "apex-input/hotkeys"] +usb = ["apex-hardware/usb"] +hotkeys = ["apex-input/hotkeys"] engine = ["apex-engine"] +sysinfo = ["dep:sysinfo"] debug = [] - - diff --git a/settings.toml b/settings.toml index 6afe98c..bc03cff 100644 --- a/settings.toml +++ b/settings.toml @@ -17,3 +17,20 @@ enabled = true # Valid choices are "gbp", "usd" and "eur" # Default is USD currency = "eur" + +[sysinfo] +enabled = true +# The polling interval for system stats in milliseconds. +polling_interval = 1500 +# The maximum value for the net I/O stat bar (in MiB), used for scaling its fill +# net_load_max = 100 +# The maximum value for the cpu frequency stat bar (in GHz), used for scaling its fill +# cpu_frequency_max = 7 +# The maximum value for the temperature stat bar (in degC), used for scaling its fill +# temperature_max = 100 +# Network interface name used in network I/O stat bar +# To find values for this config in Linux, use the `ip link` command +# net_interface_name = "eth0" +# sensor name used in temperature stat bar +# To find values for this config in Linux, use the `sensors` command +# sensor_name = "asus_wmi_sensors CPU Temperature" diff --git a/src/main.rs b/src/main.rs index b4c616c..dea3830 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ pub async fn main() -> Result<()> { #[cfg(all(feature = "usb", target_family = "unix", not(feature = "engine")))] let mut device = USBDevice::try_connect()?; - #[cfg(any(feature = "usb", feature = "engine"))] + #[cfg(feature = "hotkeys")] let hkm = apex_input::InputManager::new(tx.clone()); #[cfg(all(feature = "engine"))] @@ -96,7 +96,9 @@ pub async fn main() -> Result<()> { })?; scheduler.start(rx, settings).await?; - #[cfg(any(feature = "usb", feature = "engine"))] + + #[cfg(feature = "hotkeys")] drop(hkm); + Ok(()) } diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 7bdaf64..513021f 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -3,3 +3,5 @@ pub(crate) mod clock; pub(crate) mod coindesk; #[cfg(any(feature = "dbus-support", target_os = "windows"))] pub(crate) mod music; +#[cfg(feature = "sysinfo")] +pub(crate) mod sysinfo; diff --git a/src/providers/sysinfo.rs b/src/providers/sysinfo.rs new file mode 100644 index 0000000..a5b5c74 --- /dev/null +++ b/src/providers/sysinfo.rs @@ -0,0 +1,206 @@ +use crate::{ + render::{display::ContentProvider, scheduler::ContentWrapper}, + scheduler::CONTENT_PROVIDERS, +}; +use anyhow::Result; +use apex_hardware::FrameBuffer; +use async_stream::try_stream; +use num_traits::{pow, Pow}; + +use config::Config; +use embedded_graphics::{ + primitives::{Rectangle, Primitive, PrimitiveStyle}, + geometry::Point, + mono_font::{ascii, MonoTextStyle}, + pixelcolor::BinaryColor, + text::{renderer::TextRenderer, Baseline, Text}, + Drawable, +}; +use futures::Stream; +use linkme::distributed_slice; +use log::{info, warn}; +use tokio::{ + time, + time::{Duration, MissedTickBehavior}, +}; + +use sysinfo::{ + System, SystemExt, + RefreshKind, CpuRefreshKind, + CpuExt, NetworkData, NetworkExt, NetworksExt, ComponentExt +}; + +#[doc(hidden)] +#[distributed_slice(CONTENT_PROVIDERS)] +pub static PROVIDER_INIT: fn(&Config) -> Result> = register_callback; + +fn tick() -> i64 { + chrono::offset::Utc::now().timestamp_millis() +} + +#[doc(hidden)] +#[allow(clippy::unnecessary_wraps)] +fn register_callback(config: &Config) -> Result> { + info!("Registering Sysinfo display source."); + + let refreshes = RefreshKind::new(). + with_cpu(CpuRefreshKind::everything()). + with_components_list(). + with_components(). + with_networks_list(). + with_networks(). + with_memory(); + let sys = System::new_with_specifics(refreshes); + + let tick = tick(); + let last_tick = 0; + + Ok(Box::new(Sysinfo { + sys, tick, last_tick, refreshes, + polling_interval: config.get_int("sysinfo.polling_interval").unwrap_or(2000) as u64, + net_load_max: config.get_float("sysinfo.net_load_max").unwrap_or(100.0), + cpu_frequency_max: config.get_float("sysinfo.cpu_frequency_max").unwrap_or(7.0), + temperature_max: config.get_float("sysinfo.temperature_max").unwrap_or(100.0), + net_interface_name: config.get_str("sysinfo.net_interface_name").unwrap_or("eth0".to_string()), + sensor_name: config.get_str("sysinfo.sensor_name").unwrap_or("hwmon0 CPU Temperature".to_string()), + })) +} + +struct Sysinfo { + sys: System, + refreshes: RefreshKind, + + tick: i64, + last_tick: i64, + + polling_interval: u64, + + net_load_max: f64, + cpu_frequency_max: f64, + temperature_max: f64, + + net_interface_name: String, + sensor_name: String, +} + +impl Sysinfo { + pub fn render(&mut self) -> Result { + self.poll(); + + let load = self.sys.global_cpu_info().cpu_usage() as f64; + let freq = self.sys.global_cpu_info().frequency() as f64 / 1000.0; + let mem_used = self.sys.used_memory() as f64 / pow(1024, 3) as f64; + + let mut buffer = FrameBuffer::new(); + + self.render_stat(0, &mut buffer, format!("C: {:>4.0}%", load), load / 100.0)?; + self.render_stat(1, &mut buffer, format!("F: {:>4.2}G", freq), freq / self.cpu_frequency_max)?; + self.render_stat(2, &mut buffer, format!("M: {:>4.1}G", mem_used), self.sys.used_memory() as f64 / self.sys.total_memory() as f64)?; + + self.sys.networks().iter().find(|(name, _)| + **name == self.net_interface_name + ).map(|t| t.1).map(|n| { + let net_direction = if n.received() > n.transmitted() {"I"} else {"O"}; + + let (net_load, net_load_power, net_load_unit) = self.calculate_max_net_rate(n); + let mut adjusted_net_load = format!("{:.4}", (net_load / 1024_f64.pow(net_load_power)).to_string()); + + if adjusted_net_load.ends_with(".") { + adjusted_net_load = adjusted_net_load.replace(".", ""); + } + + self.render_stat(3, &mut buffer, format!("{}: {:>4}{}", net_direction, adjusted_net_load, net_load_unit), net_load / (self.net_load_max * 1024_f64.pow(2))) + }).unwrap_or_else(|| { + warn!("couldn't find net interface `{}`", self.net_interface_name); + Ok(()) + })?; + + self.sys.components().iter().find(|component| + component.label() == self.sensor_name + ).map(|c| { + self.render_stat(4, &mut buffer, format!("T: {:>4.1}C", c.temperature()), c.temperature() as f64 / self.temperature_max) + }).unwrap_or_else(|| { + warn!("couldn't find sensor `{}`", self.sensor_name); + Ok(()) + })?; + + Ok(buffer) + } + + fn calculate_max_net_rate(&self, net : &NetworkData) -> (f64, i32, &str) { + let max_diff = std::cmp::max(net.received(), net.transmitted()) as f64; + let max_rate = max_diff / ((self.tick - self.last_tick) as f64 / 1000.0); + + match max_rate { + r if r > 1024_f64.pow(3) => (r, 3, "G"), + r if r > 1024_f64.pow(2) => (r, 2, "M"), + r if r > 1024_f64.pow(1) => (r, 1, "k"), + r => (r, 0, "B") + } + } + + fn poll(&mut self) { + self.sys.refresh_specifics(self.refreshes); + + self.last_tick = self.tick; + self.tick = tick(); + } + + fn render_stat(&self, slot: i32, buffer: &mut FrameBuffer, text : String, fill : f64) -> Result<()> { + let style = MonoTextStyle::new(&ascii::FONT_4X6, BinaryColor::On); + let metrics = style.measure_string(&text, Point::zero(), Baseline::Top); + + let slot_y = slot*8 + 1; + + Text::with_baseline( + &text, + Point::new(0, slot_y), + style, + Baseline::Top, + ) + .draw(buffer)?; + + let bar_start: i32 = metrics.bounding_box.size.width as i32 + 2; + let border_style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + let fill_style = PrimitiveStyle::with_fill(BinaryColor::On); + let fill_width = if fill.is_infinite() { 0 } else { + (fill * (127 - bar_start) as f64).floor() as i32 + }; + + Rectangle::with_corners( + Point::new(bar_start, slot_y), + Point::new(127, slot_y + 6) + ).into_styled(border_style) + .draw(buffer)?; + + Rectangle::with_corners( + Point::new(bar_start + 1, slot_y + 1), + Point::new(bar_start + fill_width, slot_y + 5) + ).into_styled(fill_style) + .draw(buffer)?; + + Ok(()) + } +} + +impl ContentProvider for Sysinfo { + type ContentStream<'a> = impl Stream> + 'a; + + fn stream<'this>(&'this mut self) -> Result> { + let mut interval = time::interval(Duration::from_millis(self.polling_interval)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + Ok(try_stream! { + loop { + if let Ok(image) = self.render() { + yield image; + } + interval.tick().await; + } + }) + } + + fn name(&self) -> &'static str { + "sysinfo" + } +}