feat: sensor identifier mapping

Closes #18
This commit is contained in:
Markus Zehnder
2025-09-01 09:05:42 +02:00
parent 078dc267d0
commit 7ac543e644
9 changed files with 165 additions and 25 deletions
+36 -1
View File
@@ -13,6 +13,7 @@ use log::{info, warn};
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 +119,9 @@ 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>>,
}
impl MonitorConfig {
@@ -146,10 +150,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 +305,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
+39 -15
View File
@@ -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_key_value_file, start_file_slurper};
use asterctl::{cfg, img};
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
@@ -26,7 +26,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 +60,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 +136,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 +175,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,6 +192,20 @@ 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)?;
cfg.set_sensor_mapping(mapping);
} else {
info!("Sensor mapping file {mapping_cfg:?} not found");
}
Ok(cfg)
}
+6 -6
View File
@@ -80,7 +80,7 @@ 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()) {
warn!("Failed to read sensor file {path:?}: {e}");
continue;
}
@@ -113,7 +113,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);
}
for entry in fs::read_dir(path)? {
@@ -121,7 +121,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)
{
warn!("Failed to read sensor file {path:?}: {e}");
}
@@ -139,11 +139,11 @@ 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.
///
/// 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>,
) -> anyhow::Result<()> {