Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/sysinfo #30

Merged
merged 4 commits into from
Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.features": ["default", "sysinfo"]
}
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand All @@ -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 = []


17 changes: 17 additions & 0 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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(())
}
2 changes: 2 additions & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
206 changes: 206 additions & 0 deletions src/providers/sysinfo.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn ContentWrapper>> = register_callback;

fn tick() -> i64 {
chrono::offset::Utc::now().timestamp_millis()
}

#[doc(hidden)]
#[allow(clippy::unnecessary_wraps)]
fn register_callback(config: &Config) -> Result<Box<dyn ContentWrapper>> {
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<FrameBuffer> {
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<Item = Result<FrameBuffer>> + 'a;

fn stream<'this>(&'this mut self) -> Result<Self::ContentStream<'this>> {
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"
}
}