refactor: project structure (#9)
Split up project into multiple crates and use a Cargo workspace.
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder
|
||||
|
||||
#![forbid(non_ascii_idents)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
use asterctl::cfg::{MonitorConfig, Panel, load_custom_panel};
|
||||
use asterctl::render::PanelRenderer;
|
||||
use asterctl::sensors::start_file_slurper;
|
||||
use asterctl::{cfg, img};
|
||||
use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use log::{debug, error, info};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// AOOSTAR WTR MAX and GEM12+ PRO screen control.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Serial device, for example "/dev/cu.usbserial-AB0KOHLS". Takes priority over --usb option.
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// USB serial UART "vid:pid" in hex notation (lsusb output). Default: 416:90A1
|
||||
#[arg(short, long)]
|
||||
usb: Option<String>,
|
||||
|
||||
/// Switch display on and exit. This will show the last displayed image.
|
||||
#[arg(long)]
|
||||
on: bool,
|
||||
|
||||
/// Switch display off and exit.
|
||||
#[arg(long)]
|
||||
off: bool,
|
||||
|
||||
/// Image to display, other sizes than 960x376 will be scaled.
|
||||
#[arg(short, long)]
|
||||
image: Option<String>,
|
||||
|
||||
/// AOOSTAR-X json configuration file to parse.
|
||||
///
|
||||
/// The configuration file will be loaded from the `config_dir` directory if no full path is
|
||||
/// specified.
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Include one or more additional custom panels into the base configuration.
|
||||
///
|
||||
/// Specify the path to the panel directory containing panel.json and fonts / img subdirectories.
|
||||
#[arg(short, long)]
|
||||
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>,
|
||||
|
||||
/// Font directory for fonts specified in the `config` file. Default: `./fonts`
|
||||
#[arg(long)]
|
||||
font_dir: Option<PathBuf>,
|
||||
|
||||
/// Single sensor value input file or directory for multiple sensor input files.
|
||||
/// Default: `./cfg/sensors`
|
||||
#[arg(long)]
|
||||
sensor_path: Option<PathBuf>,
|
||||
|
||||
/// Switch off display n seconds after loading image or running demo.
|
||||
#[arg(short, long)]
|
||||
off_after: Option<u32>,
|
||||
|
||||
/// Test mode: only write to the display without checking response.
|
||||
#[arg(short, long)]
|
||||
write_only: bool,
|
||||
|
||||
/// Test mode: save changed images in ./out folder.
|
||||
#[arg(short, long)]
|
||||
save: bool,
|
||||
|
||||
/// Simulate serial port for testing and development, `--device` and `--usb` options are ignored.
|
||||
#[arg(long)]
|
||||
simulate: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// initialize display with given UART port parameter
|
||||
let mut builder = AooScreenBuilder::new();
|
||||
builder.no_init_check(args.write_only);
|
||||
let mut screen = if args.simulate {
|
||||
builder.simulate()?
|
||||
} else if let Some(device) = args.device {
|
||||
builder.open_device(&device)?
|
||||
} else if let Some(usb) = args.usb {
|
||||
builder.open_usb_id(&usb)?
|
||||
} else {
|
||||
builder.open_default()?
|
||||
};
|
||||
|
||||
// process simple commands
|
||||
if args.off {
|
||||
screen.off()?;
|
||||
return Ok(());
|
||||
} else if args.on {
|
||||
screen.on()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// switch on screen for remaining commands
|
||||
screen.init()?;
|
||||
|
||||
if let Some(config) = args.config {
|
||||
info!("Starting sensor panel mode");
|
||||
let img_save_path = if args.save {
|
||||
let img_save_path = PathBuf::from("out");
|
||||
fs::create_dir_all(&img_save_path)?;
|
||||
Some(img_save_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cfg_dir = args.config_dir.unwrap_or_else(|| "cfg".into());
|
||||
let cfg = load_configuration(&config, &cfg_dir, args.panels)?;
|
||||
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()),
|
||||
img_save_path,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(image) = args.image {
|
||||
info!("Loading and displaying background image {image}...");
|
||||
let rgb_img = img::load_image(&image, Some(DISPLAY_SIZE))?.to_rgb8();
|
||||
let timestamp = Instant::now();
|
||||
screen.send_image(&rgb_img)?;
|
||||
debug!("Image sent in {}ms", timestamp.elapsed().as_millis());
|
||||
}
|
||||
|
||||
if let Some(off) = args.off_after {
|
||||
info!("Switching off display in {off}s");
|
||||
sleep(Duration::from_secs(off as u64));
|
||||
screen.off()?;
|
||||
}
|
||||
|
||||
info!("Bye bye!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_configuration<P: AsRef<Path>>(
|
||||
config: P,
|
||||
config_dir: P,
|
||||
panels: Option<Vec<PathBuf>>,
|
||||
) -> anyhow::Result<MonitorConfig> {
|
||||
let config = config.as_ref();
|
||||
let config_dir = config_dir.as_ref();
|
||||
|
||||
let mut cfg = if config.is_absolute() {
|
||||
cfg::load_cfg(config)?
|
||||
} else {
|
||||
cfg::load_cfg(config_dir.join(config))?
|
||||
};
|
||||
|
||||
if let Some(panels) = panels {
|
||||
for panel in panels {
|
||||
cfg.include_custom_panel(load_custom_panel(panel)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
fn run_sensor_panel<B: Into<PathBuf>>(
|
||||
screen: &mut AooScreen,
|
||||
mut cfg: MonitorConfig,
|
||||
config_dir: B,
|
||||
font_dir: B,
|
||||
sensor_path: B,
|
||||
img_save_path: Option<B>,
|
||||
) -> anyhow::Result<()> {
|
||||
let font_dir = font_dir.into();
|
||||
let config_dir = config_dir.into();
|
||||
let img_save_path = img_save_path.map(|p| p.into());
|
||||
|
||||
let mut renderer = PanelRenderer::new(DISPLAY_SIZE, &font_dir, &config_dir);
|
||||
if let Some(img_save_path) = &img_save_path {
|
||||
renderer.set_img_save_path(img_save_path);
|
||||
renderer.set_save_render_img(true);
|
||||
// renderer.set_save_processed_pic(true);
|
||||
// renderer.set_save_progress_layer(true);
|
||||
}
|
||||
|
||||
let sensor_values: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
start_file_slurper(sensor_path, sensor_values.clone())?;
|
||||
|
||||
let refresh = Duration::from_millis((cfg.setup.refresh * 1000f32) as u64);
|
||||
|
||||
let switch_time = cfg
|
||||
.setup
|
||||
.switch_time
|
||||
.as_deref()
|
||||
.and_then(|v| f32::from_str(v).ok())
|
||||
.map(|v| Duration::from_millis((v * 1000.0) as u64))
|
||||
.unwrap_or(Duration::from_secs(5));
|
||||
|
||||
// panel switching loop
|
||||
loop {
|
||||
let panel = cfg
|
||||
.get_next_active_panel()
|
||||
.ok_or(anyhow!("No active panel"))?;
|
||||
|
||||
info!("Switching panel: {}", panel.friendly_name());
|
||||
let panel_switch_time = Instant::now();
|
||||
|
||||
// active panel refresh loop
|
||||
let mut refresh_count = 1;
|
||||
loop {
|
||||
let upd_start_time = Instant::now();
|
||||
|
||||
if img_save_path.is_some() {
|
||||
renderer.set_img_suffix(format!("-{refresh_count:02}"));
|
||||
}
|
||||
|
||||
// Keeping the read lock during panel rendering should be ok, otherwise we could always clone the HashMap
|
||||
let values = sensor_values.read().expect("RwLock is poisoned");
|
||||
update_panel(screen, &mut renderer, panel, &values)?;
|
||||
drop(values);
|
||||
|
||||
let elapsed = upd_start_time.elapsed();
|
||||
if refresh > elapsed {
|
||||
sleep(refresh - elapsed);
|
||||
}
|
||||
|
||||
if panel_switch_time.elapsed() >= switch_time {
|
||||
break;
|
||||
}
|
||||
|
||||
refresh_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_panel(
|
||||
screen: &mut AooScreen,
|
||||
renderer: &mut PanelRenderer,
|
||||
panel: &Panel,
|
||||
values: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
debug!("Displaying panel '{}'...", panel.friendly_name());
|
||||
|
||||
match renderer.render(panel, values) {
|
||||
Ok(image) => screen.send_image(&image)?,
|
||||
Err(e) => error!("Error rendering panel '{}': {e:?}", panel.friendly_name()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user