Merge pull request #20 from zehnm/feat/18-sensor-id-mapping
Sensor identifier mapping, sensor filters, internal date time sensors
This commit is contained in:
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run demo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --bin asterctl -- --demo -c monitor.json" />
|
||||
<option name="command" value="run --bin demo -- -c monitor.json" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sysinfo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
q <option name="command" value="run --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt" />
|
||||
<option name="command" value="run --bin aster-sysinfo -- --console --out ./cfg/sensors/sysinfo.txt" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sysinfo repeat" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt --refresh 3" />
|
||||
<option name="command" value="run --bin aster-sysinfo -- --console --out ./cfg/sensors/sysinfo.txt --refresh 3" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
Generated
+62
-8
@@ -42,6 +42,15 @@ dependencies = [
|
||||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.20"
|
||||
@@ -150,6 +159,7 @@ dependencies = [
|
||||
"ab_glyph",
|
||||
"anyhow",
|
||||
"asterctl-lcd",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"image",
|
||||
@@ -157,6 +167,7 @@ dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -284,6 +295,19 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.46"
|
||||
@@ -612,6 +636,30 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.6"
|
||||
@@ -1989,7 +2037,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -2010,7 +2058,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
@@ -2022,7 +2070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
@@ -2054,6 +2102,12 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -2061,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2070,7 +2124,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2079,7 +2133,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2122,7 +2176,7 @@ version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
@@ -2139,7 +2193,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Filter out specified sensor keys of the corresponding sensor file without the `-filter` suffix.
|
||||
#
|
||||
# Sensor filter for: aster-sysinfo
|
||||
#
|
||||
# Format: one RegEx entry per line.
|
||||
# Empty lines and lines starting with # are filtered out.
|
||||
|
||||
# remove all temperature sensor units
|
||||
^temperature_.*#unit
|
||||
@@ -0,0 +1,46 @@
|
||||
# Mapping sensor labels from sensor value providers to panel definition labels.
|
||||
#
|
||||
# Mapping definition for: aster-sysinfo
|
||||
#
|
||||
# Key = label identifier used in panel definition
|
||||
# Value = label identifier used in sensor providers
|
||||
|
||||
cpu_percent: cpu_usage_percent
|
||||
cpu_temperature: temperature_cpu
|
||||
memory_usage: mem_usage_percent
|
||||
memory_Temperature: temperature_memory
|
||||
gpu_temperature: temperature_gpu
|
||||
|
||||
# replace network_###_ with the desired interface: enp100s0f0np0 / enp100s0f1np1 / enp102s0 / enp103s0 etc
|
||||
net_upload_speed: network_enp100s0f1np1_upload_speed
|
||||
net_download_speed: network_enp100s0f1np1_download_speed
|
||||
net_ip_address: network_enp100s0f1np1_address0
|
||||
|
||||
# Individual storage_ssd / hdd[x] disk info doesn't seem very useful for a NAS.
|
||||
# Usually there are different arrays / fs mounts. But that's easy to map now!
|
||||
storage_ssd[0]['temperature']: temperature_nvme_Composite_KINGSTON_OM8PGP41024Q-A0
|
||||
storage_ssd[0]['used']: storage_ssd[0]_usage_percent
|
||||
storage_ssd[1]['temperature']: temperature_nvme_Composite_Samsung_SSD_990_EVO_Plus_2TB
|
||||
storage_ssd[1]['used']: storage_ssd[1]_usage_percent
|
||||
# storage_ssd[2]['temperature']:
|
||||
storage_ssd[2]['used']: storage_ssd[2]_usage_percent
|
||||
# storage_ssd[3]['temperature']:
|
||||
storage_ssd[3]['used']: storage_ssd[3]_usage_percent
|
||||
# storage_ssd[4]['temperature']:
|
||||
storage_ssd[4]['used']: storage_ssd[4]_usage_percent
|
||||
# storage_hdd[0]['temperature']:
|
||||
storage_hdd[0]['used']: storage_hdd[0]_usage_percent
|
||||
# storage_hdd[1]['temperature']:
|
||||
storage_hdd[1]['used']: storage_hdd[1]_usage_percent
|
||||
# storage_hdd[2]['temperature']:
|
||||
storage_hdd[2]['used']: storage_hdd[2]_usage_percent
|
||||
# storage_hdd[3]['temperature']:
|
||||
storage_hdd[3]['used']: storage_hdd[3]_usage_percent
|
||||
# storage_hdd[4]['temperature']:
|
||||
storage_hdd[4]['used']: storage_hdd[4]_usage_percent
|
||||
# storage_hdd[5]['temperature']:
|
||||
storage_hdd[5]['used']: storage_hdd[5]_usage_percent
|
||||
|
||||
# TODO not (yet) available in aster-sysinfo
|
||||
# gpu_core:
|
||||
# motherboard_temperature:
|
||||
@@ -224,6 +224,12 @@ impl SysinfoSource {
|
||||
);
|
||||
}
|
||||
|
||||
add_sensor(
|
||||
sensors,
|
||||
"cpu_usage_percent".to_string(),
|
||||
format!("{:.2}", self.sys.global_cpu_usage()),
|
||||
);
|
||||
|
||||
let load_avg = System::load_average();
|
||||
add_sensor(sensors, "load_avg_one", format!("{:.2}", load_avg.one));
|
||||
add_sensor(sensors, "load_avg_five", format!("{:.2}", load_avg.five));
|
||||
@@ -265,6 +271,40 @@ impl SysinfoSource {
|
||||
);
|
||||
|
||||
// System information:
|
||||
let up_secs = System::uptime();
|
||||
let up_days = up_secs / 86400;
|
||||
let up_hours = (up_secs - (up_days * 86400)) / 3600;
|
||||
let up_mins = (up_secs - (up_days * 86400) - (up_hours * 3600)) / 60;
|
||||
add_sensor(sensors, "system_uptime_sec", up_secs);
|
||||
/*
|
||||
Time to look into ftl for i18n
|
||||
The coreutils project did a lot of work that could be used:
|
||||
https://github.com/uutils/coreutils/blob/main/src/uucore/src/lib/mods/locale.rs
|
||||
Then this would be the easy way to format the time, just uses a lot of setup code:
|
||||
|
||||
uptime-format = { $days ->
|
||||
[0] { $time }
|
||||
[one] { $days } day, { $time }
|
||||
*[other] { $days } days { $time }
|
||||
}
|
||||
|
||||
translate!(
|
||||
"uptime-format",
|
||||
"days" => up_days,
|
||||
"time" => format!("{up_hours:02}:{up_mins:02}")
|
||||
)
|
||||
*/
|
||||
let day_string = match up_days {
|
||||
0 => "",
|
||||
1 => "1 day, ",
|
||||
n => &format!("{n} days "),
|
||||
};
|
||||
add_sensor(
|
||||
sensors,
|
||||
"system_uptime",
|
||||
format!("{day_string}{up_hours:02}:{up_mins:02}"),
|
||||
);
|
||||
|
||||
if let Some(name) = System::name() {
|
||||
add_sensor(sensors, "system_name", name);
|
||||
}
|
||||
@@ -361,8 +401,8 @@ impl SysinfoSource {
|
||||
// component.label(), component.type_id(), component.id());
|
||||
}
|
||||
|
||||
// TODO add unit as a separate sensor?
|
||||
add_sensor(sensors, label, format!("{temperature:.1} °C"));
|
||||
add_sensor(sensors, format!("{label}#unit"), "°C");
|
||||
add_sensor(sensors, label, format!("{temperature:.1}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ asterctl-lcd = { path = "../asterctl-lcd", version = "0.2.0" }
|
||||
|
||||
anyhow = "1.0.98"
|
||||
clap = { version = "4.5.42", features = ["derive"] }
|
||||
chrono = "0.4"
|
||||
image = "0.25.6"
|
||||
imageproc = { version = "0.25.0", default-features = false }
|
||||
ab_glyph = { version = "0.2.31", default-features = false, features = ["std"] }
|
||||
@@ -25,6 +26,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.142"
|
||||
serde_repr = "0.1.20"
|
||||
once_cell = "1.21.3"
|
||||
regex = "1.11.2"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26"
|
||||
|
||||
@@ -10,9 +10,11 @@ use anyhow::Context;
|
||||
use image::{Rgb, Rgba};
|
||||
use imageproc::definitions::HasWhite;
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::Deref;
|
||||
@@ -118,6 +120,12 @@ pub struct MonitorConfig {
|
||||
/// Internal index of the currently active panel. 1-based!
|
||||
#[serde(skip)]
|
||||
active_panel_idx: Option<usize>,
|
||||
/// Internal sensor label mapping
|
||||
#[serde(skip)]
|
||||
sensor_mapping: Option<HashMap<String, String>>,
|
||||
/// Internal sensor filter
|
||||
#[serde(skip)]
|
||||
pub sensor_filter: Option<Vec<Regex>>,
|
||||
}
|
||||
|
||||
impl MonitorConfig {
|
||||
@@ -146,10 +154,33 @@ impl MonitorConfig {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn include_custom_panel(&mut self, panel: Panel) {
|
||||
/// Adds a custom panel to the application and maps sensor labels if applicable.
|
||||
///
|
||||
/// The panel is marked active and will be returned with [get_next_active_panel] when it is its turn.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `panel` - the Panel to include in the active panels.
|
||||
pub fn include_custom_panel(&mut self, mut panel: Panel) {
|
||||
if let Some(mapping) = &self.sensor_mapping {
|
||||
panel.map_sensor_labels(mapping);
|
||||
}
|
||||
self.panels.push(panel);
|
||||
self.active_panels.push(self.panels.len() as u32);
|
||||
}
|
||||
|
||||
/// Apply a sensor label mapping on the included panels.
|
||||
///
|
||||
/// The mapping will also be applied on any custom panel added in the future with [include_custom_panel].
|
||||
///
|
||||
/// **Attention**: this method may only be called once at startup.
|
||||
/// Dynamically changing mappings are not supported, and the original sensor labels are not preserved.
|
||||
pub fn set_sensor_mapping(&mut self, mapping: HashMap<String, String>) {
|
||||
for panel in self.panels.iter_mut() {
|
||||
panel.map_sensor_labels(&mapping);
|
||||
}
|
||||
self.sensor_mapping = Some(mapping);
|
||||
}
|
||||
}
|
||||
|
||||
/// Web-app user login
|
||||
@@ -278,6 +309,14 @@ impl Panel {
|
||||
})
|
||||
.unwrap_or_else(|| "panel".into())
|
||||
}
|
||||
|
||||
fn map_sensor_labels(&mut self, mapping: &HashMap<String, String>) {
|
||||
for sensor in self.sensor.iter_mut() {
|
||||
if let Some(new_label) = mapping.get(&sensor.label) {
|
||||
sensor.label = new_label.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One Data Display Unit
|
||||
|
||||
+67
-16
@@ -6,7 +6,7 @@
|
||||
|
||||
use asterctl::cfg::{MonitorConfig, Panel, load_custom_panel};
|
||||
use asterctl::render::PanelRenderer;
|
||||
use asterctl::sensors::start_file_slurper;
|
||||
use asterctl::sensors::{read_filter_file, read_key_value_file, start_file_slurper};
|
||||
use asterctl::{cfg, img};
|
||||
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
|
||||
|
||||
@@ -14,6 +14,7 @@ use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use log::{debug, error, info};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -26,7 +27,7 @@ use std::time::{Duration, Instant};
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Serial device, for example "/dev/cu.usbserial-AB0KOHLS". Takes priority over --usb option.
|
||||
/// Serial device, for example, "/dev/cu.usbserial-AB0KOHLS". Takes priority over --usb option.
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
@@ -60,18 +61,24 @@ struct Args {
|
||||
panels: Option<Vec<PathBuf>>,
|
||||
|
||||
/// Configuration directory containing configuration files and background images
|
||||
/// specified in the `config` file. Default: `./cfg`
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
/// specified in the `config` file.
|
||||
#[arg(long, default_value_t = String::from("cfg"))]
|
||||
config_dir: String, // default_value_t requires Display trait which PathBuf does not implement
|
||||
|
||||
/// Font directory for fonts specified in the `config` file. Default: `./fonts`
|
||||
#[arg(long)]
|
||||
font_dir: Option<PathBuf>,
|
||||
/// Font directory for fonts specified in the `config` file.
|
||||
#[arg(long, default_value_t = String::from("fonts"))]
|
||||
font_dir: String,
|
||||
|
||||
/// Single sensor value input file or directory for multiple sensor input files.
|
||||
/// Default: `./cfg/sensors`
|
||||
#[arg(long)]
|
||||
sensor_path: Option<PathBuf>,
|
||||
#[arg(long, default_value_t = String::from("cfg/sensors"))]
|
||||
sensor_path: String,
|
||||
|
||||
/// Sensor identifier mapping file. Ignored if the file does not exist.
|
||||
///
|
||||
/// The configuration file will be loaded from the `config_dir` directory if no full path is
|
||||
/// specified.
|
||||
#[arg(long, default_value_t = String::from("sensor-mapping.cfg"))]
|
||||
sensor_mapping: String,
|
||||
|
||||
/// Switch off display n seconds after loading image or running demo.
|
||||
#[arg(short, long)]
|
||||
@@ -130,14 +137,17 @@ fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let cfg_dir = args.config_dir.unwrap_or_else(|| "cfg".into());
|
||||
let cfg = load_configuration(&config, &cfg_dir, args.panels)?;
|
||||
let cfg_dir = PathBuf::from(args.config_dir);
|
||||
let font_dir = PathBuf::from(args.font_dir);
|
||||
let sensor_path = PathBuf::from(args.sensor_path);
|
||||
let mapping_cfg = PathBuf::from(args.sensor_mapping);
|
||||
let cfg = load_configuration(&config, &cfg_dir, args.panels, &mapping_cfg)?;
|
||||
run_sensor_panel(
|
||||
&mut screen,
|
||||
cfg,
|
||||
cfg_dir,
|
||||
args.font_dir.unwrap_or_else(|| "fonts".into()),
|
||||
args.sensor_path.unwrap_or_else(|| "cfg/sensors".into()),
|
||||
font_dir,
|
||||
sensor_path,
|
||||
img_save_path,
|
||||
)?;
|
||||
return Ok(());
|
||||
@@ -166,6 +176,7 @@ fn load_configuration<P: AsRef<Path>>(
|
||||
config: P,
|
||||
config_dir: P,
|
||||
panels: Option<Vec<PathBuf>>,
|
||||
sensor_mapping: P,
|
||||
) -> anyhow::Result<MonitorConfig> {
|
||||
let config = config.as_ref();
|
||||
let config_dir = config_dir.as_ref();
|
||||
@@ -182,9 +193,45 @@ fn load_configuration<P: AsRef<Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
let sensor_mapping = sensor_mapping.as_ref();
|
||||
let mapping_cfg = if sensor_mapping.is_absolute() {
|
||||
sensor_mapping.to_path_buf()
|
||||
} else {
|
||||
config_dir.join(sensor_mapping)
|
||||
};
|
||||
if mapping_cfg.is_file() {
|
||||
let mut mapping = HashMap::new();
|
||||
read_key_value_file(&mapping_cfg, &mut mapping, None)?;
|
||||
cfg.set_sensor_mapping(mapping);
|
||||
} else {
|
||||
info!("Sensor mapping file {mapping_cfg:?} not found");
|
||||
}
|
||||
|
||||
cfg.sensor_filter = load_sensor_filter(&mapping_cfg)?;
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
fn load_sensor_filter(mapping_cfg: &Path) -> anyhow::Result<Option<Vec<Regex>>> {
|
||||
if let Some(parent) = mapping_cfg.parent()
|
||||
&& let Some(file_stem) = mapping_cfg.file_stem()
|
||||
&& let Some(extension) = mapping_cfg.extension()
|
||||
{
|
||||
let filter_file = parent
|
||||
.join(format!("{}-filter", file_stem.to_string_lossy()))
|
||||
.with_extension(extension);
|
||||
|
||||
if filter_file.is_file() {
|
||||
info!("Loading sensor filter file {filter_file:?}");
|
||||
return read_filter_file(filter_file);
|
||||
} else {
|
||||
info!("No sensor filter file {filter_file:?} available");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn run_sensor_panel<B: Into<PathBuf>>(
|
||||
screen: &mut AooScreen,
|
||||
mut cfg: MonitorConfig,
|
||||
@@ -207,7 +254,11 @@ fn run_sensor_panel<B: Into<PathBuf>>(
|
||||
|
||||
let sensor_values: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
start_file_slurper(sensor_path, sensor_values.clone())?;
|
||||
start_file_slurper(
|
||||
sensor_path,
|
||||
sensor_values.clone(),
|
||||
cfg.sensor_filter.clone(),
|
||||
)?;
|
||||
|
||||
let refresh = Duration::from_millis((cfg.setup.refresh * 1000f32) as u64);
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ use crate::cfg::{Panel, Sensor, SensorDirection, SensorMode, TextAlign};
|
||||
use crate::font::FontHandler;
|
||||
use crate::format_value;
|
||||
use crate::img::{ImageCache, Size, rotate_image};
|
||||
use crate::sensors::get_date_time_value;
|
||||
use ab_glyph::Font;
|
||||
use chrono::{DateTime, Local};
|
||||
use image::{ImageBuffer, Rgba, RgbaImage};
|
||||
use imageproc::drawing::{draw_text_mut, text_size};
|
||||
use log::{debug, error};
|
||||
@@ -160,6 +162,8 @@ impl PanelRenderer {
|
||||
values: &HashMap<String, String>,
|
||||
mut background: RgbaImage,
|
||||
) -> Result<RgbaImage, ImageProcessingError> {
|
||||
let now: DateTime<Local> = Local::now();
|
||||
|
||||
for sensor in &panel.sensor {
|
||||
let value = values.get(&sensor.label).cloned();
|
||||
let unit = values
|
||||
@@ -170,6 +174,8 @@ impl PanelRenderer {
|
||||
|
||||
if let Some(value) = value {
|
||||
self.render_sensor(&mut background, sensor, &value, &unit)?;
|
||||
} else if let Some(value) = get_date_time_value(&sensor.label, &now) {
|
||||
self.render_sensor(&mut background, sensor, &value, &unit)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+177
-10
@@ -3,11 +3,15 @@
|
||||
|
||||
//! Sensor value sources.
|
||||
//!
|
||||
//! Only implementation is a file-based value provider with simple key-value pairs.
|
||||
//! Implementations:
|
||||
//! - internal date time sensors
|
||||
//! - file-based value provider with simple key-value pairs.
|
||||
|
||||
use chrono::{DateTime, Datelike, Local, Timelike};
|
||||
use log::{debug, error, info, warn};
|
||||
use notify::event::{ModifyKind, RenameMode};
|
||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
@@ -16,6 +20,46 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, RwLock, mpsc};
|
||||
|
||||
pub fn get_date_time_value(label: &str, now: &DateTime<Local>) -> Option<String> {
|
||||
if !label.starts_with("DATE_") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let year = now.year();
|
||||
let month = format!("{:02}", now.month());
|
||||
let day = format!("{:02}", now.day());
|
||||
let hour = format!("{:02}", now.hour());
|
||||
let minute = format!("{:02}", now.minute());
|
||||
let second = format!("{:02}", now.second());
|
||||
|
||||
// same formatting logic as in AOOSTAR-X
|
||||
let value = match label {
|
||||
"DATE_year" => year.to_string(),
|
||||
"DATE_month" => month,
|
||||
"DATE_day" => day,
|
||||
"DATE_hour" => hour,
|
||||
"DATE_minute" => minute,
|
||||
"DATE_second" => second,
|
||||
"DATE_m_d_h_m_1" => format!("{month}月{day}日 {hour}:{minute}"),
|
||||
"DATE_m_d_h_m_2" => format!("{month}/{day} {hour}:{minute}"),
|
||||
"DATE_m_d_1" => format!("{month}月{day}日"),
|
||||
"DATE_m_d_2" => format!("{month}-{day}"),
|
||||
"DATE_y_m_d_1" => format!("{year}年{month}月{day}日"),
|
||||
"DATE_y_m_d_2" => format!("{year}-{month}-{day}"),
|
||||
"DATE_y_m_d_3" => format!("{year}/{month}/{day}"),
|
||||
"DATE_y_m_d_4" => format!("{year} {month} {day}"),
|
||||
"DATE_h_m_s_1" => format!("{hour}:{minute}:{second}"),
|
||||
"DATE_h_m_s_2" => format!("{hour}时{minute}分{second}秒"),
|
||||
"DATE_h_m_s_3" => format!("{hour} {minute} {second}"),
|
||||
"DATE_h_m_1" => format!("{hour}时{minute}分"),
|
||||
"DATE_h_m_2" => format!("{hour} : {minute}"),
|
||||
"DATE_h_m_3" => format!("{hour}:{minute}"),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(value)
|
||||
}
|
||||
|
||||
/// Read all sensor value source files from the given path and stort monitoring for changes.
|
||||
///
|
||||
/// The source path is either a single sensor source file or a directory containing multiple sensor
|
||||
@@ -28,17 +72,19 @@ use std::sync::{Arc, RwLock, mpsc};
|
||||
///
|
||||
/// * `source_path`: Single source file path or a directory path.
|
||||
/// * `values`: a shared, reader-writer lock protected HashMap
|
||||
/// * `sensor_filter`: Optional list of regex filters to filter out matching sensor keys.
|
||||
///
|
||||
/// returns: Result<(), Error>
|
||||
pub fn start_file_slurper<P: Into<PathBuf>>(
|
||||
source_path: P,
|
||||
values: Arc<RwLock<HashMap<String, String>>>,
|
||||
sensor_filter: Option<Vec<Regex>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let dir_path = source_path.into();
|
||||
// read existing file(s)
|
||||
{
|
||||
let mut val = values.write().expect("Failed to lock values");
|
||||
read_path(&dir_path, val.deref_mut())?;
|
||||
read_path(&dir_path, val.deref_mut(), sensor_filter.as_deref())?;
|
||||
}
|
||||
|
||||
let file_values = values.clone();
|
||||
@@ -54,7 +100,7 @@ pub fn start_file_slurper<P: Into<PathBuf>>(
|
||||
}
|
||||
};
|
||||
|
||||
info!("Starting sensor file watcher for {dir_path:?}");
|
||||
info!("Starting sensor file watcher for {dir_path:?} with filter {sensor_filter:?}");
|
||||
if let Err(e) = watcher.watch(&dir_path, RecursiveMode::NonRecursive) {
|
||||
error!("Failed to start file watcher: {e}");
|
||||
exit(1);
|
||||
@@ -80,7 +126,9 @@ pub fn start_file_slurper<P: Into<PathBuf>>(
|
||||
debug!("Modified sensor file ({kind:?}): {path:?}");
|
||||
let mut val = file_values.write().expect("Poisoned sensor RwLock");
|
||||
|
||||
if let Err(e) = read_from_file(path, val.deref_mut()) {
|
||||
if let Err(e) =
|
||||
read_key_value_file(path, val.deref_mut(), sensor_filter.as_deref())
|
||||
{
|
||||
warn!("Failed to read sensor file {path:?}: {e}");
|
||||
continue;
|
||||
}
|
||||
@@ -103,9 +151,14 @@ pub fn start_file_slurper<P: Into<PathBuf>>(
|
||||
///
|
||||
/// * `path`: Single source file path or a directory path.
|
||||
/// * `values`: HashMap to store all read key-value pairs.
|
||||
/// * `sensor_filter`: Optional list of regex filters to filter out matching sensor keys.
|
||||
///
|
||||
/// returns: Result<(), Error>
|
||||
fn read_path<P: AsRef<Path>>(path: P, values: &mut HashMap<String, String>) -> anyhow::Result<()> {
|
||||
fn read_path<P: AsRef<Path>>(
|
||||
path: P,
|
||||
values: &mut HashMap<String, String>,
|
||||
sensor_filter: Option<&[Regex]>,
|
||||
) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.try_exists()? {
|
||||
@@ -113,7 +166,7 @@ fn read_path<P: AsRef<Path>>(path: P, values: &mut HashMap<String, String>) -> a
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
return read_from_file(path, values);
|
||||
return read_key_value_file(path, values, sensor_filter);
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(path)? {
|
||||
@@ -121,7 +174,7 @@ fn read_path<P: AsRef<Path>>(path: P, values: &mut HashMap<String, String>) -> a
|
||||
|
||||
if path.is_file()
|
||||
&& path.extension().unwrap_or_default() == "txt"
|
||||
&& let Err(e) = read_from_file(&path, values)
|
||||
&& let Err(e) = read_key_value_file(&path, values, sensor_filter)
|
||||
{
|
||||
warn!("Failed to read sensor file {path:?}: {e}");
|
||||
}
|
||||
@@ -139,13 +192,15 @@ fn read_path<P: AsRef<Path>>(path: P, values: &mut HashMap<String, String>) -> a
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path`: file path to read
|
||||
/// * `values`: HashMap to store read key-value pairs.
|
||||
/// * `path`: file path to read.
|
||||
/// * `values`: HashMap to insert key-value pairs from the file.
|
||||
/// * `sensor_filter`: Optional list of regex filters to filter out matching sensor keys.
|
||||
///
|
||||
/// returns: Result<(), Error>
|
||||
fn read_from_file<P: AsRef<Path>>(
|
||||
pub fn read_key_value_file<P: AsRef<Path>>(
|
||||
path: P,
|
||||
values: &mut HashMap<String, String>,
|
||||
sensor_filter: Option<&[Regex]>,
|
||||
) -> anyhow::Result<()> {
|
||||
debug!("Reading sensor file {:?}", path.as_ref());
|
||||
|
||||
@@ -159,6 +214,13 @@ fn read_from_file<P: AsRef<Path>>(
|
||||
continue;
|
||||
}
|
||||
if let Some((key, value)) = line.split_once(':') {
|
||||
if let Some(filter) = sensor_filter
|
||||
&& is_filtered(key, filter)
|
||||
{
|
||||
debug!("Filtered: {key}");
|
||||
continue;
|
||||
}
|
||||
|
||||
values.insert(key.trim().to_string(), value.trim().to_string());
|
||||
} else {
|
||||
warn!("Skipping invalid entry in sensor value file: {line}");
|
||||
@@ -167,3 +229,108 @@ fn read_from_file<P: AsRef<Path>>(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_filtered(key: &str, filters: &[Regex]) -> bool {
|
||||
filters.iter().any(|re| re.is_match(key))
|
||||
}
|
||||
|
||||
/// Read the sensor filter configuration file.
|
||||
///
|
||||
/// This is a simple text file containing multiple RegEx expressions.
|
||||
/// - one RegEx per line
|
||||
/// - Empty lines are skipped
|
||||
/// - Lines starting with # are skipped
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path`: file path to read.
|
||||
///
|
||||
/// returns: None if the file is empty or contains no valid RegEx expressions.
|
||||
///
|
||||
pub fn read_filter_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Option<Vec<Regex>>> {
|
||||
debug!("Reading sensor filter file {:?}", path.as_ref());
|
||||
|
||||
let mut filters = Vec::new();
|
||||
let file = fs::File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
match Regex::new(line) {
|
||||
Ok(re) => {
|
||||
filters.push(re);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Skipping invalid filter in sensor filter file: {line}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filters.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(filters))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn is_filtered_does_not_filter_without_filters() {
|
||||
let key = "foobar";
|
||||
let filters = Vec::new();
|
||||
assert!(!is_filtered(key, &filters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit_extension_filter() {
|
||||
let key = "temperature_cpu#unit";
|
||||
let filters = vec![Regex::new("^temperature_.*#unit").unwrap()];
|
||||
assert!(is_filtered(key, &filters));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(vec!["^foo$"])]
|
||||
#[case(vec!["^bar"])]
|
||||
#[case(vec!["other"])]
|
||||
#[case(vec!["123", "bla", "other"])]
|
||||
fn is_filtered_does_not_filter_without_a_match(#[case] filters: Vec<&str>) {
|
||||
let key = "foobar";
|
||||
let filters: Vec<Regex> = filters
|
||||
.iter()
|
||||
.map(|f| Regex::new(f).expect("Invalid regex"))
|
||||
.collect();
|
||||
assert!(
|
||||
!is_filtered(key, &filters),
|
||||
"Filter {filters:?} should not match {key}"
|
||||
);
|
||||
//
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(vec!["foo"])]
|
||||
#[case(vec!["bar"])]
|
||||
#[case(vec!["^.+bar"])]
|
||||
#[case(vec!["123", "foo", "other"])]
|
||||
#[case(vec!["bar", "123"])]
|
||||
#[case(vec!["^.+bar", "other"])]
|
||||
fn is_filtered_matches_filters(#[case] filters: Vec<&str>) {
|
||||
let key = "foobar";
|
||||
let filters: Vec<Regex> = filters
|
||||
.iter()
|
||||
.map(|f| Regex::new(f).expect("Invalid regex"))
|
||||
.collect();
|
||||
assert!(
|
||||
is_filtered(key, &filters),
|
||||
"Filter {filters:?} match match {key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- [Progress Sensor](sensor/cfg/mode3_progress.md)
|
||||
- [Pointer Sensor](sensor/cfg/mode4_pointer.md)
|
||||
- [Sensor Value Provider](sensor/provider/README.md)
|
||||
- [Internal Date Time](sensor/provider/internal_date_time.md)
|
||||
- [Text File Data Source](sensor/provider/text_file.md)
|
||||
- [Shell Scripts](sensor/provider/shell_scripts.md)
|
||||
- [aster-sysinfo Tool](sensor/provider/sysinfo.md)
|
||||
|
||||
@@ -53,6 +53,13 @@ Options:
|
||||
Single sensor value input file or directory for multiple sensor input files.
|
||||
Default: `./cfg/sensors`
|
||||
|
||||
--sensor-mapping <SENSOR_MAPPING>
|
||||
Sensor identifier mapping file. Ignored if the file does not exist.
|
||||
|
||||
The configuration file will be loaded from the `config_dir` directory if no full path is specified.
|
||||
|
||||
[default: sensor-mapping.cfg]
|
||||
|
||||
-o, --off-after <OFF_AFTER>
|
||||
Switch off display n seconds after loading image or running demo
|
||||
|
||||
|
||||
+50
-2
@@ -15,10 +15,12 @@ Different sensor modes are supported:
|
||||
|
||||
## Sensor Data Sources
|
||||
|
||||
The sensor value reading is separated from the `asterctl` tool.
|
||||
The sensor value reading is separated from the `asterctl` tool, with the exception of some internal sensors:
|
||||
|
||||
- Internal [date time sensors](provider/internal_date_time.md)
|
||||
|
||||
Sensor values are provided in separate text files and are automatically read when the file changes.
|
||||
Only the file data source is supported at the moment, other sources like pipes, sockets etc. might be supported later.
|
||||
Only the file data source is supported at the moment; other sources like pipes, sockets, etc. might be supported later.
|
||||
|
||||
- [Text file data source](provider/text_file.md)
|
||||
|
||||
@@ -26,3 +28,49 @@ Only the file data source is supported at the moment, other sources like pipes,
|
||||
|
||||
- Proof of concept [Linux shell scripts](provider/shell_scripts.md)
|
||||
- [aster-sysinfo tool](provider/sysinfo.md)
|
||||
|
||||
### Sensor Identifier Mapping
|
||||
|
||||
The original AOOSTAR-X software uses very weird label identifiers (actually sometimes even a composite key depending on
|
||||
the data source), which are likely based on an internal JSON structure.
|
||||
|
||||
To easily use original custom sensor panels with various sensor data sources, a sensor identifier mapping file can be used.
|
||||
|
||||
The mapping file is a simple text file with one identifier mapping per line:
|
||||
- Key = label identifier used in panel definition
|
||||
- Value = label identifier used in sensor providers
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
cpu_temperature: temperature_cpu
|
||||
```
|
||||
|
||||
This maps the `temperature_cpu` sensor from the `aster-sysinfo` tool to the `cpu_temperature` sensor used in the
|
||||
AOOSTAR-X panel definitions.
|
||||
|
||||
Usage example:
|
||||
```shell
|
||||
asterctl --config monitor.json --sensor-mapping sensor-mapping/sysinfo-to-aoostar.cfg
|
||||
```
|
||||
|
||||
### Sensor Filter
|
||||
|
||||
Sensor entries in the text file can be filtered by regular expressions defined in the sensor filter file having the
|
||||
same name as the sensor identifier mapping file, but with the `-filter` suffix in the file name.
|
||||
|
||||
Example:
|
||||
- Sensor identifier mapping file: `sensor-mapping/sysinfo-to-aoostar.cfg`
|
||||
- Sensor filter file: `sensor-mapping/sysinfo-to-aoostar-filter.cfg`
|
||||
|
||||
The filter file is a simple text file with one regular expression per line:
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# remove all temperature sensor units
|
||||
temperature_.*#unit
|
||||
```
|
||||
|
||||
This removes all sensors starting with `temperature_` and ending with `#unit`, which will make sure that all the
|
||||
temperature sensors will be rendered without the unit text suffix on the display panel.
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
# Sensor Value Provider
|
||||
|
||||
- Internal [date time sensors](internal_date_time.md)
|
||||
- Proof of concept [Linux shell scripts](shell_scripts.md)
|
||||
- [aster-sysinfo tool](sysinfo.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Internal Date Time Sensors
|
||||
|
||||
## Individual Components
|
||||
|
||||
- `DATE_year`: `{year}`
|
||||
- `DATE_month`: `{month}` with leading zero
|
||||
- `DATE_day`: `{day}` with leading zero
|
||||
- `DATE_hour`: `{hour}` 24h format with leading zero
|
||||
- `DATE_minute`: `{minute}` with leading zero
|
||||
- `DATE_second`: `{second}` with leading zero
|
||||
|
||||
## Month/Day with Hour/Minute
|
||||
- `DATE_m_d_h_m_1`: `{month}月{day}日 {hour}:{minute}`
|
||||
- `DATE_m_d_h_m_2`: `{month}/{day} {hour}:{minute}`
|
||||
|
||||
## Month/Day Only
|
||||
- `DATE_m_d_1`: `{month}月{day}日`
|
||||
- `DATE_m_d_2`: `{month}-{day}`
|
||||
|
||||
## Year/Month/Day
|
||||
- `DATE_y_m_d_1`: `{year}年{month}月{day}日`
|
||||
- `DATE_y_m_d_2`: `{year}-{month}-{day}`
|
||||
- `DATE_y_m_d_3`: `{year}/{month}/{day}`
|
||||
- `DATE_y_m_d_4`: `{year} {month} {day}`
|
||||
|
||||
## Hour/Minute/Second
|
||||
- `DATE_h_m_s_1`: `{hour}:{minute}:{second}`
|
||||
- `DATE_h_m_s_2`: `{hour}时{minute}分{second}秒`
|
||||
- `DATE_h_m_s_3`: `{hour} {minute} {second}`
|
||||
|
||||
## Hour/Minute Only
|
||||
- `DATE_h_m_1`: `{hour}时{minute}分`
|
||||
- `DATE_h_m_2`: `{hour} : {minute}`
|
||||
- `DATE_h_m_3`: `{hour}:{minute}`
|
||||
Reference in New Issue
Block a user