Compare commits

...

3 commits

Author SHA1 Message Date
Fl1tzi 4c1c75e7d8 Update README.md 2024-02-26 00:38:08 +00:00
Fl1tzi 282721ea92
small note 2024-02-26 01:28:19 +01:00
Fl1tzi db03650065
major memory improvement by moving the image cache to fs 2024-02-26 01:22:02 +01:00
10 changed files with 116 additions and 128 deletions

View file

@ -19,5 +19,6 @@ rusttype = "0.9.3"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
font-loader = "0.11.0" font-loader = "0.11.0"
clru = "0.6.1" once_cell = "1.19.0"
once_cell = "1.19.0" ring = "0.17"
base64 = "0.21"

View file

@ -6,7 +6,7 @@ This is a long term project. See [v0.1](https://codeberg.org/Fl1tzi/dach-decker/
# Microdeck # Microdeck
Microdeck is an open-source software designed for configuring Stream Decks with ease and versatility. It aims to address common pain points found in existing solutions by offering improved rendering speeds, greater customization options, and a focus on stability. Microdeck is an open-source software designed for configuring Stream Decks with ease and versatility. It aims to address common pain points found in existing solutions by offering improved rendering speeds, greater customization options, and a focus on stability. Other devices may be added in the future (see [driver](https://codeberg.org/Fl1tzi/deck-driver)) if needed. The only requirement is that the key layout is rectangular and that there are displays on the keys (otherwise other pieces of software would be more useful).
## Key features ## Key features

View file

@ -3,11 +3,9 @@ use crate::{
modules::{start_module, HostEvent, MODULE_REGISTRY}, modules::{start_module, HostEvent, MODULE_REGISTRY},
unwrap_or_error, unwrap_or_error,
}; };
use clru::CLruCache;
use deck_driver as streamdeck; use deck_driver as streamdeck;
use hidapi::HidApi; use hidapi::HidApi;
use image::DynamicImage; use std::{collections::HashMap, fmt::Display, sync::Arc};
use std::{collections::HashMap, fmt::Display, num::NonZeroUsize, sync::Arc};
use streamdeck::{ use streamdeck::{
asynchronous::{AsyncStreamDeck, ButtonStateUpdate}, asynchronous::{AsyncStreamDeck, ButtonStateUpdate},
info::Kind, info::Kind,
@ -16,10 +14,7 @@ use streamdeck::{
use tokio::{ use tokio::{
process::Command, process::Command,
runtime::Runtime, runtime::Runtime,
sync::{ sync::mpsc::{self, error::TrySendError},
mpsc::{self, error::TrySendError},
Mutex,
},
}; };
use tracing::{debug, error, info_span, trace, warn}; use tracing::{debug, error, info_span, trace, warn};
@ -46,11 +41,6 @@ impl Display for Device {
} }
} }
/// image cache
///
/// (button, key), image data
pub type ImageCache = CLruCache<(u8, u32), Arc<DynamicImage>>;
/// Handles everything related to a single device /// Handles everything related to a single device
pub struct Device { pub struct Device {
modules: HashMap<u8, ModuleController>, modules: HashMap<u8, ModuleController>,
@ -61,7 +51,6 @@ pub struct Device {
selected_space: Option<String>, selected_space: Option<String>,
is_dead_handle: Arc<std::sync::Mutex<bool>>, is_dead_handle: Arc<std::sync::Mutex<bool>>,
serial: String, serial: String,
image_cache: Arc<Mutex<ImageCache>>,
} }
impl Device { impl Device {
@ -104,9 +93,6 @@ impl Device {
selected_space: None, selected_space: None,
is_dead_handle, is_dead_handle,
serial, serial,
image_cache: Arc::new(Mutex::new(CLruCache::new(
NonZeroUsize::new(button_count.into()).unwrap(),
))),
}) })
} }
@ -147,11 +133,10 @@ impl Device {
let ser = self.serial.clone(); let ser = self.serial.clone();
let dev = self.device.clone(); let dev = self.device.clone();
let b = btn.clone(); let b = btn.clone();
let image_cache = self.image_cache.clone();
runtime.spawn(async move { runtime.spawn(
start_module(ser, b, *module, dev, module_receiver, image_cache).await async move { start_module(ser, b, *module, dev, module_receiver).await },
}); );
} }
// if the receiver already dropped the listener then just directly insert none. // if the receiver already dropped the listener then just directly insert none.
// Optimizes performance because the key_listener just does not try to send the event. // Optimizes performance because the key_listener just does not try to send the event.

View file

@ -5,11 +5,13 @@ use image::imageops;
use image::{io::Reader, DynamicImage, ImageBuffer, Rgb, RgbImage}; use image::{io::Reader, DynamicImage, ImageBuffer, Rgb, RgbImage};
use imageproc::drawing::draw_text_mut; use imageproc::drawing::draw_text_mut;
use rusttype::Scale; use rusttype::Scale;
use std::io; use std::{io, path::Path};
use tracing::trace;
/// Loads an image from a path /// Retrieve an image from a path
#[allow(dead_code)] #[allow(dead_code)]
pub fn load_image(path: String) -> io::Result<DynamicImage> { pub fn retrieve_image(path: &Path) -> io::Result<DynamicImage> {
trace!("Retrieving image from filesystem");
Ok(Reader::open(path)? Ok(Reader::open(path)?
.decode() .decode()
.expect("Unable to decode image")) .expect("Unable to decode image"))

View file

@ -6,6 +6,7 @@ use hidapi::HidApi;
use rusttype::Font; use rusttype::Font;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs,
process::exit, process::exit,
sync::{Arc, Mutex, OnceLock}, sync::{Arc, Mutex, OnceLock},
time::Duration, time::Duration,
@ -24,6 +25,7 @@ mod modules;
mod type_definition; mod type_definition;
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new(); pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
pub static CACHE_DIR: &'static str = "microdeck";
#[macro_export] #[macro_export]
macro_rules! skip_if_none { macro_rules! skip_if_none {
@ -64,6 +66,14 @@ fn main() {
} }
}; };
// create cache dir
if let Some(mut path) = dirs::cache_dir() {
path.push(CACHE_DIR);
if path.exists() == false {
fs::create_dir(path).expect("Could not create cache directory");
}
}
// load font // load font
let font = match config.global.font_family { let font = match config.global.font_family {
Some(ref f) => font_loader::system_fonts::get( Some(ref f) => font_loader::system_fonts::get(

View file

@ -10,21 +10,24 @@ use self::space::Space;
// other things // other things
use crate::config::{Button, ButtonConfigError}; use crate::config::{Button, ButtonConfigError};
use crate::device::ImageCache; use crate::image_rendering::{retrieve_image, ImageBuilder};
use crate::image_rendering::{load_image, ImageBuilder};
use ::image::imageops::FilterType; use ::image::imageops::FilterType;
use ::image::io::Reader as ImageReader;
use ::image::DynamicImage; use ::image::DynamicImage;
use async_trait::async_trait; use async_trait::async_trait;
use base64::engine::{general_purpose, Engine};
pub use deck_driver as streamdeck; pub use deck_driver as streamdeck;
use dirs::cache_dir;
use futures_util::Future; use futures_util::Future;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use ring::digest;
use std::collections::HashMap; use std::collections::HashMap;
use std::{error::Error, pin::Pin, sync::Arc}; use std::{error::Error, path::PathBuf, pin::Pin, sync::Arc};
use streamdeck::info::ImageFormat; use streamdeck::info::ImageFormat;
use streamdeck::info::Kind; use streamdeck::info::Kind;
use streamdeck::AsyncStreamDeck; use streamdeck::AsyncStreamDeck;
use streamdeck::StreamDeckError; use streamdeck::StreamDeckError;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::mpsc;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
pub static MODULE_REGISTRY: Lazy<ModuleRegistry> = Lazy::new(|| ModuleRegistry::default()); pub static MODULE_REGISTRY: Lazy<ModuleRegistry> = Lazy::new(|| ModuleRegistry::default());
@ -41,7 +44,7 @@ pub enum HostEvent {
pub type ModuleObject = Box<dyn Module + Send + Sync>; pub type ModuleObject = Box<dyn Module + Send + Sync>;
pub type ModuleFuture = pub type ModuleFuture =
Pin<Box<dyn Future<Output = Result<ModuleObject, ButtonConfigError>> + Send>>; Pin<Box<dyn Future<Output = Result<ModuleObject, ButtonConfigError>> + Send>>;
pub type ModuleInitFunction = fn(Arc<Button>, ModuleCache) -> ModuleFuture; pub type ModuleInitFunction = fn(Arc<Button>) -> ModuleFuture;
pub type ModuleMap = HashMap<&'static str, ModuleInitFunction>; pub type ModuleMap = HashMap<&'static str, ModuleInitFunction>;
@ -83,21 +86,15 @@ pub async fn start_module(
module_init_function: ModuleInitFunction, module_init_function: ModuleInitFunction,
device: Arc<AsyncStreamDeck>, device: Arc<AsyncStreamDeck>,
br: ChannelReceiver, br: ChannelReceiver,
image_cache: Arc<Mutex<ImageCache>>,
) { ) {
debug!("STARTED"); debug!("STARTED");
let mc = ModuleCache::new(
image_cache,
button.index,
device.kind().key_image_format().size,
);
let da = DeviceAccess::new(device.clone(), button.index).await; let da = DeviceAccess::new(device.clone(), button.index).await;
// init // init
// //
// This function should be called after the config was checked, // This function should be called after the config was checked,
// otherwise it will panic and the module wont be started. // otherwise it will panic and the module wont be started.
let mut module = match module_init_function(button.clone(), mc).await { let mut module = match module_init_function(button.clone()).await {
Ok(m) => m, Ok(m) => m,
Err(e) => panic!("{}", e), Err(e) => panic!("{}", e),
}; };
@ -121,63 +118,76 @@ pub async fn start_module(
} }
} }
/// A wrapper around [ImageCache] to provide easy access to values in the device cache /// Loads an image from the system or retrieves it from the cache. If
pub struct ModuleCache { /// the provided image is not already in the cache it will be inserted.
image_cache: Arc<Mutex<ImageCache>>, #[allow(dead_code)]
button_index: u8, pub async fn load_image(path: PathBuf, resolution: (usize, usize)) -> Option<DynamicImage> {
/// Resolution of the deck (required for optimization of storage space) // hash the image
resolution: (usize, usize), let mut image = tokio::task::spawn_blocking(move || retrieve_image(&path))
.await
.unwrap()
.ok()?;
let image_hash = hash_image(image.as_bytes());
if let Some(image) = get_image_from_cache(&image_hash, resolution) {
trace!("Cached image is available");
return Some(image);
}
// TODO prevent multiple buttons from resizing the same image at the same time (performance
// improvement)
let image = tokio::task::spawn_blocking(move || {
trace!("Resizing image");
image = image.resize_exact(
resolution.0 as u32,
resolution.1 as u32,
FilterType::Lanczos3,
);
trace!("Resizing finished");
let mut path = match cache_dir() {
Some(dir) => dir,
None => return None, // System does not provide cache
};
path.push("microdeck");
path.push(image_cache_file_name(&image_hash, resolution));
image.save(path).ok()?;
Some(image)
})
.await
.unwrap()?;
Some(image.into())
} }
impl ModuleCache { /// File name for a cached image
pub fn new( ///
image_cache: Arc<Mutex<ImageCache>>, /// `<hash>-<height>x<width>`
button_index: u8, pub fn image_cache_file_name(image_hash: &str, resolution: (usize, usize)) -> String {
resolution: (usize, usize), format!("{}-{}x{}.png", image_hash, resolution.0, resolution.1)
) -> Self { }
ModuleCache {
image_cache,
button_index,
resolution,
}
}
/// Load an image from the [ImageCache] or create a new one and insert it into the [ImageCache]. pub fn hash_image(data: &[u8]) -> String {
/// Returns None if no image was found. let mut context = digest::Context::new(&digest::SHA256);
/// context.update(data);
/// index: Provide an index where your data is cached. With this number the value can be let hash = context.finish();
/// accessed again. Use [DeviceAccess::get_image_cached()] for just getting the data. general_purpose::STANDARD.encode(hash)
#[allow(dead_code)] }
pub async fn load_image(&mut self, path: String, index: u32) -> Option<Arc<DynamicImage>> {
if let Some(image) = self.get_image(index).await {
Some(image)
} else {
trace!("Decoding image");
let mut image = tokio::task::spawn_blocking(move || load_image(path))
.await
.unwrap()
.ok()?;
image = image.resize_exact(
self.resolution.0 as u32,
self.resolution.1 as u32,
FilterType::Lanczos3,
);
trace!("Decoding finished");
let image = Arc::new(image);
let mut data = self.image_cache.lock().await;
data.put((self.button_index, index), image.clone());
trace!("Wrote data into cache (new size: {})", data.len());
drop(data);
Some(image.into())
}
}
/// Just try to retrieve a value from the key (index) in the [ImageCache]. /// Try to retrieve an image from the cache. Will return None if
#[allow(dead_code)] /// the image was not cached yet (or is not accessible)
pub async fn get_image(&self, index: u32) -> Option<Arc<DynamicImage>> { /// or if the system does not provide a [dirs::cache_dir].
let mut data = self.image_cache.lock().await; #[allow(dead_code)]
data.get(&(self.button_index, index)).cloned() pub fn get_image_from_cache(image_hash: &str, resolution: (usize, usize)) -> Option<DynamicImage> {
} let mut path = match cache_dir() {
Some(dir) => dir,
None => return None, // System does not provide cache
};
path.push("microdeck");
path.push(image_cache_file_name(image_hash, resolution));
Some(ImageReader::open(path).ok()?.decode().ok()?)
} }
/// Wrapper to provide easier access to the Deck /// Wrapper to provide easier access to the Deck
@ -240,14 +250,11 @@ pub trait Module: Sync + Send {
/// ///
/// This function should **not** panic as the panic will not be catched and therefore would be /// This function should **not** panic as the panic will not be catched and therefore would be
/// not noticed. /// not noticed.
async fn new( async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError>
config: Arc<Button>,
mut cache: ModuleCache,
) -> Result<ModuleObject, ButtonConfigError>
where where
Self: Sized; Self: Sized;
/// Function for actually running the module and interacting with the device. Errors that /// Function for actually running the module and interacting with the device. Errors that
/// happen here should be mostly prevented. /// happen here should be mostly prevented as they are not properly handled.
async fn run( async fn run(
&mut self, &mut self,
device: DeviceAccess, device: DeviceAccess,

View file

@ -3,7 +3,6 @@ use super::ButtonConfigError;
use super::ChannelReceiver; use super::ChannelReceiver;
use super::DeviceAccess; use super::DeviceAccess;
use super::Module; use super::Module;
use super::ModuleCache;
use super::ModuleObject; use super::ModuleObject;
use super::ReturnError; use super::ReturnError;
use async_trait::async_trait; use async_trait::async_trait;
@ -13,10 +12,7 @@ pub struct Blank;
#[async_trait] #[async_trait]
impl Module for Blank { impl Module for Blank {
async fn new( async fn new(_config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
_config: Arc<Button>,
_cache: ModuleCache,
) -> Result<ModuleObject, ButtonConfigError> {
Ok(Box::new(Blank {})) Ok(Box::new(Blank {}))
} }

View file

@ -3,8 +3,7 @@ use std::sync::Arc;
use crate::config::Button; use crate::config::Button;
use super::{ use super::{
ButtonConfigError, ChannelReceiver, DeviceAccess, HostEvent, Module, ModuleCache, ModuleObject, ButtonConfigError, ChannelReceiver, DeviceAccess, HostEvent, Module, ModuleObject, ReturnError,
ReturnError,
}; };
use crate::image_rendering::wrap_text; use crate::image_rendering::wrap_text;
@ -25,10 +24,7 @@ pub struct Counter {
#[async_trait] #[async_trait]
impl Module for Counter { impl Module for Counter {
async fn new( async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
config: Arc<Button>,
_cache: ModuleCache,
) -> Result<ModuleObject, ButtonConfigError> {
let title = config.parse_module("title", " ".to_string()).res()?; let title = config.parse_module("title", " ".to_string()).res()?;
let title_size = config.parse_module("title_size", 15.0).res()?; let title_size = config.parse_module("title_size", 15.0).res()?;
let number_size = config.parse_module("number_size", 25.0).res()?; let number_size = config.parse_module("number_size", 25.0).res()?;

View file

@ -1,38 +1,34 @@
use super::load_image;
use super::Button; use super::Button;
use super::ButtonConfigError; use super::ButtonConfigError;
use super::ChannelReceiver; use super::ChannelReceiver;
use super::DeviceAccess; use super::DeviceAccess;
use super::Module; use super::Module;
use super::ModuleCache;
use super::ModuleObject; use super::ModuleObject;
use super::ReturnError; use super::ReturnError;
use crate::image_rendering::ImageBuilder; use crate::image_rendering::ImageBuilder;
use async_trait::async_trait; use async_trait::async_trait;
use image::DynamicImage; use std::{path::PathBuf, sync::Arc};
use std::sync::Arc;
pub struct Image { pub struct Image {
image: Arc<DynamicImage>,
scale: f32, scale: f32,
path: PathBuf,
} }
#[async_trait] #[async_trait]
impl Module for Image { impl Module for Image {
async fn new( async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
config: Arc<Button>,
mut cache: ModuleCache,
) -> Result<ModuleObject, ButtonConfigError> {
let path = config.parse_module("path", String::new()).required()?; let path = config.parse_module("path", String::new()).required()?;
let scale = config.parse_module("scale", 100.0).res()?; let scale = config.parse_module("scale", 100.0).res()?;
let image = cache let path = PathBuf::from(path);
.load_image(path, 1) if path.exists() == false {
.await return Err(ButtonConfigError::General(
.ok_or(ButtonConfigError::General(
"Image was not found".to_string(), "Image was not found".to_string(),
))?; ));
};
Ok(Box::new(Image { image, scale })) Ok(Box::new(Image { scale, path }))
} }
async fn run( async fn run(
@ -41,9 +37,8 @@ impl Module for Image {
_button_receiver: ChannelReceiver, _button_receiver: ChannelReceiver,
) -> Result<(), ReturnError> { ) -> Result<(), ReturnError> {
let (h, w) = streamdeck.resolution(); let (h, w) = streamdeck.resolution();
let img = (*self.image).clone();
let img = ImageBuilder::new(h, w) let img = ImageBuilder::new(h, w)
.set_image(img) .set_image(load_image(self.path.clone(), (h, w)).await.unwrap())
.set_image_scale(self.scale) .set_image_scale(self.scale)
.build(); .build();
streamdeck.write_img(img).await.unwrap(); streamdeck.write_img(img).await.unwrap();

View file

@ -3,7 +3,6 @@ use super::ButtonConfigError;
use super::ChannelReceiver; use super::ChannelReceiver;
use super::DeviceAccess; use super::DeviceAccess;
use super::Module; use super::Module;
use super::ModuleCache;
use super::ModuleObject; use super::ModuleObject;
use super::ReturnError; use super::ReturnError;
use crate::image_rendering::{create_error_image, ImageBuilder}; use crate::image_rendering::{create_error_image, ImageBuilder};
@ -17,10 +16,7 @@ pub struct Space {
#[async_trait] #[async_trait]
impl Module for Space { impl Module for Space {
async fn new( async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
config: Arc<Button>,
_cache: ModuleCache,
) -> Result<ModuleObject, ButtonConfigError> {
let name = config.parse_module("name", "Unknown".to_string()).res()?; let name = config.parse_module("name", "Unknown".to_string()).res()?;
Ok(Box::new(Space { name })) Ok(Box::new(Space { name }))
} }