From bf5d1fdf3a52f6c1b96e0ff9a13ed58b896d9c86 Mon Sep 17 00:00:00 2001 From: Fl1tzi Date: Fri, 8 Dec 2023 22:51:01 +0100 Subject: [PATCH] fix blocking code, change configuration, other small changes --- Cargo.toml | 2 - example/config.json | 41 +++++++++++++++++ src/config.rs | 14 +++++- src/device.rs | 21 ++++++--- src/main.rs | 99 ++++++++++++++++++++++++++++-------------- src/modules/counter.rs | 4 +- 6 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 example/config.json diff --git a/Cargo.toml b/Cargo.toml index 217fcac..5b851c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT" hidapi = "2.2.0" deck-driver = { git = "https://codeberg.org/Fl1tzi/deck-driver.git", branch = "main", features = ["async"] } tokio = { version = "1", features = ["full"] } -log = "0.4" dirs = "4.0.0" serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" @@ -19,6 +18,5 @@ imageproc = "0.23.0" rusttype = "0.9.3" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -lazy_static = "1.4.0" font-loader = "0.11.0" clru = "0.6.1" diff --git a/example/config.json b/example/config.json new file mode 100644 index 0000000..0f04256 --- /dev/null +++ b/example/config.json @@ -0,0 +1,41 @@ +{ + "global": {}, + "devices": [ + { + "serial": "*", + "buttons": [ + { + "index": 1, + "module": "counter", + "options": { + "font_size": "20", + "title": "Test\ntest", + "increment": "500" + } + }, + { + "index": 0, + "module": "space", + "options": { + "name": "folder1" + } + } + ] + } + ], + "spaces": { + "folder1": [ + { + "index": 1, + "module": "space", + "options": { + "name": "home" + } + }, + { + "index": 2, + "module": "counter" + } + ] + } +} diff --git a/src/config.rs b/src/config.rs index eed995a..858cc91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,8 +15,10 @@ use std::{ use tracing::debug; /// The name of the folder which holds the config +pub const CONFIG_ENVIRONMENT_VARIABLE: &'static str = "MICRODECK_CONFIG"; pub const CONFIG_FOLDER_NAME: &'static str = "microdeck"; pub const CONFIG_FILE: &'static str = "config.json"; +pub const DEFAULT_DEVICE_REFRESH_CYCLE: u64 = 3; /// Combination of buttons acting as a folder which a device can switch to pub type Space = Vec>; @@ -28,7 +30,7 @@ pub type Space = Vec>; /// CONFIGURATION #[derive(Deserialize, Debug)] pub struct Config { - pub global: Option, + pub global: GlobalConfig, pub devices: Vec, pub spaces: Arc>, } @@ -37,6 +39,13 @@ pub struct Config { #[derive(Deserialize, Debug)] pub struct GlobalConfig { pub font_family: Option, + /// Seconds when new devices get detected + #[serde(default = "default_device_refresh_cycle")] + pub device_list_refresh_cycle: u64, +} + +fn default_device_refresh_cycle() -> u64 { + DEFAULT_DEVICE_REFRESH_CYCLE } /// configuration of a single device with its default page @@ -169,6 +178,7 @@ impl Button { } /// Just retrieve the raw string from a key without trying to parse the value + #[allow(dead_code)] pub fn raw_module(&self, key: &String) -> Option<&String> { self.options.get(key) } @@ -180,7 +190,7 @@ impl Button { #[tracing::instrument] pub fn load_config() -> Result { - let config_file: PathBuf = match env::var_os("MICRODECK_CONFIG") { + let config_file: PathBuf = match env::var_os(CONFIG_ENVIRONMENT_VARIABLE) { Some(path) => { debug!("Using env variable: {:?}", path); PathBuf::from(path) diff --git a/src/device.rs b/src/device.rs index 65b529b..d5aaf24 100644 --- a/src/device.rs +++ b/src/device.rs @@ -59,6 +59,7 @@ pub struct Device { config: DeviceConfig, spaces: Arc>, selected_space: Option, + is_dead_handle: Arc>, serial: String, image_cache: Arc>, } @@ -68,6 +69,7 @@ impl Device { serial: String, kind: Kind, device_conf: DeviceConfig, + is_dead_handle: Arc>, spaces: Arc>, hid: &HidApi, ) -> Result { @@ -100,6 +102,7 @@ impl Device { config: device_conf, spaces, selected_space: None, + is_dead_handle, serial, image_cache: Arc::new(Mutex::new(CLruCache::new( NonZeroUsize::new(button_count.into()).unwrap(), @@ -171,17 +174,25 @@ impl Device { self.serial.clone() } - /// shutdown the runtime and therefore kill all the modules + /// Shutdown the runtime and therefore kill all the modules and + /// note death in field [self.is_dead_handle]. fn drop(&mut self) { if let Some(handle) = self.modules_runtime.take() { handle.shutdown_background(); } self.modules = HashMap::new(); + *self + .is_dead_handle + .lock() + .expect("Unable to lock Mutex to signal drop of device") = true; } - /// if this device holds any modules - pub fn has_modules(&self) -> bool { - !self.modules.is_empty() + /// shutdown the runtime and therefore kill all the modules. + fn shutdown_modules(&mut self) { + if let Some(handle) = self.modules_runtime.take() { + handle.shutdown_background(); + } + self.modules = HashMap::new(); } /// listener for button press changes on the device @@ -217,7 +228,7 @@ impl Device { } else { self.selected_space = Some(name) } - self.drop(); + self.shutdown_modules(); for key in 0..self.device.kind().key_count() { self.device.clear_button_image(key).await.unwrap(); } diff --git a/src/main.rs b/src/main.rs index 82b352e..4b2d43f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,11 @@ use rusttype::Font; use std::{ collections::HashMap, process::exit, - sync::{Arc, OnceLock}, + sync::{Arc, Mutex, OnceLock}, + thread, time::Duration, }; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, trace, warn}; use tracing_subscriber::{ self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, }; @@ -65,21 +66,14 @@ fn main() { }; // load font - // TODO: make this prettier - let font = match config.global { - Some(ref g) => { - if let Some(family) = &g.font_family { - font_loader::system_fonts::get( - &mut FontPropertyBuilder::new().family(family.as_str()).build(), - ) - .unwrap_or_else(|| { - warn!("Unable to load custom font"); - load_system_font() - }) - } else { - load_system_font() - } - } + let font = match config.global.font_family { + Some(ref f) => font_loader::system_fonts::get( + &mut FontPropertyBuilder::new().family(f.as_str()).build(), + ) + .unwrap_or_else(|| { + warn!("Unable to load custom font"); + load_system_font() + }), None => load_system_font(), }; @@ -104,37 +98,71 @@ fn load_system_font() -> (Vec, i32) { .expect("Unable to load system monospace font. Please specify a custom font in the config.") } -pub async fn start(config: Config, mut hid: HidApi) { - let mut devices: HashMap = HashMap::new(); +struct DeviceHandle { + handle: tokio::task::JoinHandle, + is_dead: Arc>, +} + +impl DeviceHandle { + fn new(handle: tokio::task::JoinHandle, is_dead: Arc>) -> Self { + DeviceHandle { handle, is_dead } + } + fn is_dead(&self) -> bool { + match self.is_dead.lock() { + Ok(guard) => *guard, + Err(_) => false, + } + } +} + +async fn start(config: Config, mut hid: HidApi) { + let mut devices: HashMap> = HashMap::new(); // devices which are not configured anyways let mut ignore_devices: Vec = Vec::new(); + let refresh_cycle = Duration::from_secs(config.global.device_list_refresh_cycle); + loop { // refresh device list + trace!("Refreshing device list"); if let Err(e) = streamdeck::refresh_device_list(&mut hid) { warn!("Cannot fetch new devices: {}", e); } else { for hw_device in streamdeck::list_devices(&hid) { - // if the device is not ignored and device is not already started - if !ignore_devices.contains(&hw_device.1) && devices.get(&hw_device.1).is_none() { - debug!("New device detected: {}", &hw_device.1); - // match regex for device serial + // if the device is not already started or the device is + // dropped + if let Some(d) = devices.get(&hw_device.1) { + if d.is_dead() { + trace!("Removing dead device {}", &hw_device.1); + d.handle.abort(); + devices.remove(&hw_device.1); + } else { + // ignore this device + continue; + } + } + if !ignore_devices.contains(&hw_device.1) { + // TODO: match regex for device serial if let Some(device_config) = config .devices .iter() .find(|d| d.serial == hw_device.1 || d.serial == "*") { - // start the device and its listener + // start the device and its functions + let is_dead = Arc::new(Mutex::new(false)); if let Some(device) = start_device( hw_device, &hid, device_config.clone(), config.spaces.clone(), + is_dead.clone(), ) .await { - devices.insert(device.serial(), device); + let serial = device.serial(); + let handle = tokio::spawn(init_device_functions(device)); + devices.insert(serial, DeviceHandle::new(handle, is_dead)); } } else { info!("The device {} is not configured.", hw_device.1); @@ -143,24 +171,29 @@ pub async fn start(config: Config, mut hid: HidApi) { } } } - - tokio::time::sleep(Duration::from_secs(2)).await; + tokio::time::sleep(refresh_cycle).await; } } -/// Start a device by initially creating the [Device] and then starting all modules and listeners for that device +/// Create [Device] modules and init listener +async fn init_device_functions(device: Device) { + let mut device = device; + device.init_modules().await; + device.key_listener().await; +} + +/// Start a device by initially creating the [Device] #[tracing::instrument(name = "device", skip_all, fields(serial = device.1))] -pub async fn start_device( +async fn start_device( device: (streamdeck::info::Kind, String), hid: &HidApi, device_config: DeviceConfig, spaces: Arc>, + is_dead: Arc>, ) -> Option { - match Device::new(device.1, device.0, device_config, spaces, &hid).await { - Ok(mut device) => { + match Device::new(device.1, device.0, device_config, is_dead, spaces, &hid).await { + Ok(device) => { info!("Connected"); - device.init_modules().await; - device.key_listener().await; Some(device) } Err(e) => { diff --git a/src/modules/counter.rs b/src/modules/counter.rs index e0b42ef..1ac214b 100644 --- a/src/modules/counter.rs +++ b/src/modules/counter.rs @@ -19,7 +19,7 @@ pub struct Counter { title: String, title_size: f32, number_size: f32, - increment: u32 + increment: u32, } #[async_trait] @@ -37,7 +37,7 @@ impl Module for Counter { title, title_size, number_size, - increment + increment, })) }