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">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Run demo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
<configuration default="false" name="Run demo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||||
<option name="buildProfileId" value="dev" />
|
<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$" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||||
<envs />
|
<envs />
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Run sysinfo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
<configuration default="false" name="Run sysinfo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||||
<option name="buildProfileId" value="dev" />
|
<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$" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||||
<envs />
|
<envs />
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Run sysinfo repeat" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
<configuration default="false" name="Run sysinfo repeat" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||||
<option name="buildProfileId" value="dev" />
|
<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$" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||||
<envs />
|
<envs />
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
|
|||||||
Generated
+62
-8
@@ -42,6 +42,15 @@ dependencies = [
|
|||||||
"equator",
|
"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]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.20"
|
version = "0.6.20"
|
||||||
@@ -150,6 +159,7 @@ dependencies = [
|
|||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"asterctl-lcd",
|
"asterctl-lcd",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"image",
|
"image",
|
||||||
@@ -157,6 +167,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"regex",
|
||||||
"rstest",
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -284,6 +295,19 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.46"
|
version = "4.5.46"
|
||||||
@@ -612,6 +636,30 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
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]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.6"
|
version = "0.25.6"
|
||||||
@@ -1989,7 +2037,7 @@ dependencies = [
|
|||||||
"windows-collections",
|
"windows-collections",
|
||||||
"windows-core",
|
"windows-core",
|
||||||
"windows-future",
|
"windows-future",
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
"windows-numerics",
|
"windows-numerics",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2010,7 +2058,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
"windows-result",
|
"windows-result",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
]
|
]
|
||||||
@@ -2022,7 +2070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core",
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
"windows-threading",
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2054,6 +2102,12 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-numerics"
|
name = "windows-numerics"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2061,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core",
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2070,7 +2124,7 @@ version = "0.3.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2079,7 +2133,7 @@ version = "0.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2122,7 +2176,7 @@ version = "0.53.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
"windows_aarch64_gnullvm 0.53.0",
|
"windows_aarch64_gnullvm 0.53.0",
|
||||||
"windows_aarch64_msvc 0.53.0",
|
"windows_aarch64_msvc 0.53.0",
|
||||||
"windows_i686_gnu 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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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();
|
let load_avg = System::load_average();
|
||||||
add_sensor(sensors, "load_avg_one", format!("{:.2}", load_avg.one));
|
add_sensor(sensors, "load_avg_one", format!("{:.2}", load_avg.one));
|
||||||
add_sensor(sensors, "load_avg_five", format!("{:.2}", load_avg.five));
|
add_sensor(sensors, "load_avg_five", format!("{:.2}", load_avg.five));
|
||||||
@@ -265,6 +271,40 @@ impl SysinfoSource {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// System information:
|
// 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() {
|
if let Some(name) = System::name() {
|
||||||
add_sensor(sensors, "system_name", name);
|
add_sensor(sensors, "system_name", name);
|
||||||
}
|
}
|
||||||
@@ -361,8 +401,8 @@ impl SysinfoSource {
|
|||||||
// component.label(), component.type_id(), component.id());
|
// component.label(), component.type_id(), component.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add unit as a separate sensor?
|
add_sensor(sensors, format!("{label}#unit"), "°C");
|
||||||
add_sensor(sensors, label, format!("{temperature:.1} °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"
|
anyhow = "1.0.98"
|
||||||
clap = { version = "4.5.42", features = ["derive"] }
|
clap = { version = "4.5.42", features = ["derive"] }
|
||||||
|
chrono = "0.4"
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
imageproc = { version = "0.25.0", default-features = false }
|
imageproc = { version = "0.25.0", default-features = false }
|
||||||
ab_glyph = { version = "0.2.31", default-features = false, features = ["std"] }
|
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_json = "1.0.142"
|
||||||
serde_repr = "0.1.20"
|
serde_repr = "0.1.20"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
|
regex = "1.11.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.26"
|
rstest = "0.26"
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ use anyhow::Context;
|
|||||||
use image::{Rgb, Rgba};
|
use image::{Rgb, Rgba};
|
||||||
use imageproc::definitions::HasWhite;
|
use imageproc::definitions::HasWhite;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
use regex::Regex;
|
||||||
use serde::de::Visitor;
|
use serde::de::Visitor;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
@@ -118,6 +120,12 @@ pub struct MonitorConfig {
|
|||||||
/// Internal index of the currently active panel. 1-based!
|
/// Internal index of the currently active panel. 1-based!
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
active_panel_idx: Option<usize>,
|
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 {
|
impl MonitorConfig {
|
||||||
@@ -146,10 +154,33 @@ impl MonitorConfig {
|
|||||||
None
|
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.panels.push(panel);
|
||||||
self.active_panels.push(self.panels.len() as u32);
|
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
|
/// Web-app user login
|
||||||
@@ -278,6 +309,14 @@ impl Panel {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| "panel".into())
|
.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
|
/// One Data Display Unit
|
||||||
|
|||||||
+67
-16
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use asterctl::cfg::{MonitorConfig, Panel, load_custom_panel};
|
use asterctl::cfg::{MonitorConfig, Panel, load_custom_panel};
|
||||||
use asterctl::render::PanelRenderer;
|
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::{cfg, img};
|
||||||
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
|
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ use anyhow::anyhow;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -26,7 +27,7 @@ use std::time::{Duration, Instant};
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
struct Args {
|
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)]
|
#[arg(short, long)]
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
|
|
||||||
@@ -60,18 +61,24 @@ struct Args {
|
|||||||
panels: Option<Vec<PathBuf>>,
|
panels: Option<Vec<PathBuf>>,
|
||||||
|
|
||||||
/// Configuration directory containing configuration files and background images
|
/// Configuration directory containing configuration files and background images
|
||||||
/// specified in the `config` file. Default: `./cfg`
|
/// specified in the `config` file.
|
||||||
#[arg(long)]
|
#[arg(long, default_value_t = String::from("cfg"))]
|
||||||
config_dir: Option<PathBuf>,
|
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`
|
/// Font directory for fonts specified in the `config` file.
|
||||||
#[arg(long)]
|
#[arg(long, default_value_t = String::from("fonts"))]
|
||||||
font_dir: Option<PathBuf>,
|
font_dir: String,
|
||||||
|
|
||||||
/// Single sensor value input file or directory for multiple sensor input files.
|
/// Single sensor value input file or directory for multiple sensor input files.
|
||||||
/// Default: `./cfg/sensors`
|
#[arg(long, default_value_t = String::from("cfg/sensors"))]
|
||||||
#[arg(long)]
|
sensor_path: String,
|
||||||
sensor_path: Option<PathBuf>,
|
|
||||||
|
/// 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.
|
/// Switch off display n seconds after loading image or running demo.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@@ -130,14 +137,17 @@ fn main() -> anyhow::Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let cfg_dir = args.config_dir.unwrap_or_else(|| "cfg".into());
|
let cfg_dir = PathBuf::from(args.config_dir);
|
||||||
let cfg = load_configuration(&config, &cfg_dir, args.panels)?;
|
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(
|
run_sensor_panel(
|
||||||
&mut screen,
|
&mut screen,
|
||||||
cfg,
|
cfg,
|
||||||
cfg_dir,
|
cfg_dir,
|
||||||
args.font_dir.unwrap_or_else(|| "fonts".into()),
|
font_dir,
|
||||||
args.sensor_path.unwrap_or_else(|| "cfg/sensors".into()),
|
sensor_path,
|
||||||
img_save_path,
|
img_save_path,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -166,6 +176,7 @@ fn load_configuration<P: AsRef<Path>>(
|
|||||||
config: P,
|
config: P,
|
||||||
config_dir: P,
|
config_dir: P,
|
||||||
panels: Option<Vec<PathBuf>>,
|
panels: Option<Vec<PathBuf>>,
|
||||||
|
sensor_mapping: P,
|
||||||
) -> anyhow::Result<MonitorConfig> {
|
) -> anyhow::Result<MonitorConfig> {
|
||||||
let config = config.as_ref();
|
let config = config.as_ref();
|
||||||
let config_dir = config_dir.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)
|
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>>(
|
fn run_sensor_panel<B: Into<PathBuf>>(
|
||||||
screen: &mut AooScreen,
|
screen: &mut AooScreen,
|
||||||
mut cfg: MonitorConfig,
|
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()));
|
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);
|
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::font::FontHandler;
|
||||||
use crate::format_value;
|
use crate::format_value;
|
||||||
use crate::img::{ImageCache, Size, rotate_image};
|
use crate::img::{ImageCache, Size, rotate_image};
|
||||||
|
use crate::sensors::get_date_time_value;
|
||||||
use ab_glyph::Font;
|
use ab_glyph::Font;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
use image::{ImageBuffer, Rgba, RgbaImage};
|
use image::{ImageBuffer, Rgba, RgbaImage};
|
||||||
use imageproc::drawing::{draw_text_mut, text_size};
|
use imageproc::drawing::{draw_text_mut, text_size};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
@@ -160,6 +162,8 @@ impl PanelRenderer {
|
|||||||
values: &HashMap<String, String>,
|
values: &HashMap<String, String>,
|
||||||
mut background: RgbaImage,
|
mut background: RgbaImage,
|
||||||
) -> Result<RgbaImage, ImageProcessingError> {
|
) -> Result<RgbaImage, ImageProcessingError> {
|
||||||
|
let now: DateTime<Local> = Local::now();
|
||||||
|
|
||||||
for sensor in &panel.sensor {
|
for sensor in &panel.sensor {
|
||||||
let value = values.get(&sensor.label).cloned();
|
let value = values.get(&sensor.label).cloned();
|
||||||
let unit = values
|
let unit = values
|
||||||
@@ -170,6 +174,8 @@ impl PanelRenderer {
|
|||||||
|
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
self.render_sensor(&mut background, sensor, &value, &unit)?;
|
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.
|
//! 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 log::{debug, error, info, warn};
|
||||||
use notify::event::{ModifyKind, RenameMode};
|
use notify::event::{ModifyKind, RenameMode};
|
||||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||||
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
@@ -16,6 +20,46 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::{Arc, RwLock, mpsc};
|
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.
|
/// 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
|
/// 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.
|
/// * `source_path`: Single source file path or a directory path.
|
||||||
/// * `values`: a shared, reader-writer lock protected HashMap
|
/// * `values`: a shared, reader-writer lock protected HashMap
|
||||||
|
/// * `sensor_filter`: Optional list of regex filters to filter out matching sensor keys.
|
||||||
///
|
///
|
||||||
/// returns: Result<(), Error>
|
/// returns: Result<(), Error>
|
||||||
pub fn start_file_slurper<P: Into<PathBuf>>(
|
pub fn start_file_slurper<P: Into<PathBuf>>(
|
||||||
source_path: P,
|
source_path: P,
|
||||||
values: Arc<RwLock<HashMap<String, String>>>,
|
values: Arc<RwLock<HashMap<String, String>>>,
|
||||||
|
sensor_filter: Option<Vec<Regex>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let dir_path = source_path.into();
|
let dir_path = source_path.into();
|
||||||
// read existing file(s)
|
// read existing file(s)
|
||||||
{
|
{
|
||||||
let mut val = values.write().expect("Failed to lock values");
|
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();
|
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) {
|
if let Err(e) = watcher.watch(&dir_path, RecursiveMode::NonRecursive) {
|
||||||
error!("Failed to start file watcher: {e}");
|
error!("Failed to start file watcher: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -80,7 +126,9 @@ pub fn start_file_slurper<P: Into<PathBuf>>(
|
|||||||
debug!("Modified sensor file ({kind:?}): {path:?}");
|
debug!("Modified sensor file ({kind:?}): {path:?}");
|
||||||
let mut val = file_values.write().expect("Poisoned sensor RwLock");
|
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}");
|
warn!("Failed to read sensor file {path:?}: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -103,9 +151,14 @@ pub fn start_file_slurper<P: Into<PathBuf>>(
|
|||||||
///
|
///
|
||||||
/// * `path`: Single source file path or a directory path.
|
/// * `path`: Single source file path or a directory path.
|
||||||
/// * `values`: HashMap to store all read key-value pairs.
|
/// * `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>
|
/// 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();
|
let path = path.as_ref();
|
||||||
|
|
||||||
if !path.try_exists()? {
|
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() {
|
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)? {
|
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()
|
if path.is_file()
|
||||||
&& path.extension().unwrap_or_default() == "txt"
|
&& 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}");
|
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
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `path`: file path to read
|
/// * `path`: file path to read.
|
||||||
/// * `values`: HashMap to store read key-value pairs.
|
/// * `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>
|
/// returns: Result<(), Error>
|
||||||
fn read_from_file<P: AsRef<Path>>(
|
pub fn read_key_value_file<P: AsRef<Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
values: &mut HashMap<String, String>,
|
values: &mut HashMap<String, String>,
|
||||||
|
sensor_filter: Option<&[Regex]>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
debug!("Reading sensor file {:?}", path.as_ref());
|
debug!("Reading sensor file {:?}", path.as_ref());
|
||||||
|
|
||||||
@@ -159,6 +214,13 @@ fn read_from_file<P: AsRef<Path>>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some((key, value)) = line.split_once(':') {
|
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());
|
values.insert(key.trim().to_string(), value.trim().to_string());
|
||||||
} else {
|
} else {
|
||||||
warn!("Skipping invalid entry in sensor value file: {line}");
|
warn!("Skipping invalid entry in sensor value file: {line}");
|
||||||
@@ -167,3 +229,108 @@ fn read_from_file<P: AsRef<Path>>(
|
|||||||
|
|
||||||
Ok(())
|
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)
|
- [Progress Sensor](sensor/cfg/mode3_progress.md)
|
||||||
- [Pointer Sensor](sensor/cfg/mode4_pointer.md)
|
- [Pointer Sensor](sensor/cfg/mode4_pointer.md)
|
||||||
- [Sensor Value Provider](sensor/provider/README.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)
|
- [Text File Data Source](sensor/provider/text_file.md)
|
||||||
- [Shell Scripts](sensor/provider/shell_scripts.md)
|
- [Shell Scripts](sensor/provider/shell_scripts.md)
|
||||||
- [aster-sysinfo Tool](sensor/provider/sysinfo.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.
|
Single sensor value input file or directory for multiple sensor input files.
|
||||||
Default: `./cfg/sensors`
|
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>
|
-o, --off-after <OFF_AFTER>
|
||||||
Switch off display n seconds after loading image or running demo
|
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
|
## 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.
|
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)
|
- [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)
|
- Proof of concept [Linux shell scripts](provider/shell_scripts.md)
|
||||||
- [aster-sysinfo tool](provider/sysinfo.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
|
# 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