From 474ecf4f5ebf7863752b119907b6ca490d6cc9f2 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Tue, 2 Jan 2024 20:18:47 +0100 Subject: [PATCH] commit --- Cargo.lock | 167 ++++++++++++-- deckster/Cargo.toml | 7 +- deckster/examples/full/deckster.toml | 8 +- deckster/src/main.rs | 10 +- deckster/src/model/config.rs | 59 ++++- deckster/src/model/geometry.rs | 6 +- deckster/src/model/icon_descriptor.rs | 12 +- deckster/src/model/image_filter.rs | 126 +++++++++-- .../src/model/key_modes/home_assistant.rs | 6 +- deckster/src/model/key_modes/media.rs | 6 +- deckster/src/model/key_modes/spotify.rs | 6 +- deckster/src/model/key_page.rs | 10 +- deckster/src/model/knob_modes/audio_volume.rs | 10 +- deckster/src/model/knob_page.rs | 10 +- deckster/src/model/mod.rs | 75 ++++++- deckster/src/model/rgb.rs | 53 +++-- deckster/src/runner/command.rs | 6 - deckster/src/runner/mod.rs | 208 +++++++++++++++--- deckster/src/runner/state.rs | 78 +++++++ deckster/src/runner/state/mod.rs | 34 --- loupedeck_serial/Cargo.toml | 4 +- loupedeck_serial/src/characteristics.rs | 44 ++-- loupedeck_serial/src/device.rs | 75 ++++--- loupedeck_serial/src/events.rs | 2 + loupedeck_serial/src/messages.rs | 98 +++++++-- 25 files changed, 864 insertions(+), 256 deletions(-) delete mode 100644 deckster/src/runner/command.rs create mode 100644 deckster/src/runner/state.rs delete mode 100644 deckster/src/runner/state/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 841722d..cfb1efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,25 +259,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "crossbeam-channel" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] - [[package]] name = "darling" version = "0.20.3" @@ -320,7 +301,9 @@ dependencies = [ "clap", "color-eyre", "enum-map", + "enum-ordinalize", "env_logger", + "flume", "humantime-serde", "log", "loupedeck_serial", @@ -331,6 +314,7 @@ dependencies = [ "serde_regex", "serde_with", "thiserror", + "tokio", "toml", "walkdir", ] @@ -445,12 +429,49 @@ dependencies = [ "once_cell", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "gimli" version = "0.28.1" @@ -643,6 +664,16 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -654,9 +685,9 @@ name = "loupedeck_serial" version = "0.1.0" dependencies = [ "bytes", - "crossbeam-channel", "enum-ordinalize", "enumset", + "flume", "rgb", "serialport", "thiserror", @@ -692,6 +723,15 @@ dependencies = [ "adler", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nix" version = "0.26.4" @@ -712,6 +752,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -733,6 +783,29 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "piet" version = "0.6.2" @@ -779,6 +852,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -964,6 +1046,21 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1049,6 +1146,30 @@ dependencies = [ "time-core", ] +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "num_cpus", + "parking_lot", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.8" @@ -1212,6 +1333,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.89" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index cca6121..ab8d8a3 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" color-eyre = "0.6.2" humantime-serde = "1.1.1" loupedeck_serial = { path = "../loupedeck_serial" } -piet = "0.6.2" rgb = "0.8.37" serde = { version = "1.0.193", features = ["derive"] } serde_regex = "1.1.0" @@ -19,4 +18,8 @@ log = "0.4.20" env_logger = "0.10.1" clap = { version = "4.4.12", features = ["derive"] } enum-map = "3.0.0-beta.2" -walkdir = "2.4.0" \ No newline at end of file +walkdir = "2.4.0" +tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]} +flume = "0.11.0" +enum-ordinalize = "4.3.0" +piet = "0.6.2" \ No newline at end of file diff --git a/deckster/examples/full/deckster.toml b/deckster/examples/full/deckster.toml index 72c48f8..78c82cd 100644 --- a/deckster/examples/full/deckster.toml +++ b/deckster/examples/full/deckster.toml @@ -1,20 +1,24 @@ key_pages = ["./key-pages/*"] knob_pages = ["./knob-pages/*"] -inactive_button_color = "#ff0000" -active_button_color = "#ffffff" +inactive_button_color = "#000060" +active_button_color = "#eeffff" [buttons.0] key_page = "default" +knob_page = "default" [buttons.1] key_page = "numpad" +knob_page = "default" [buttons.2] key_page = "emojis" +knob_page = "default" [buttons.3] key_page = "special_chars" +knob_page = "default" [initial] key_page = "default" diff --git a/deckster/src/main.rs b/deckster/src/main.rs index cdd55aa..65e8e3b 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; -use color_eyre::eyre::{OptionExt, WrapErr}; +use color_eyre::eyre::WrapErr; use color_eyre::Result; use walkdir::WalkDir; @@ -28,7 +28,8 @@ enum Command { }, } -pub fn main() -> Result<()> { +#[tokio::main] +pub async fn main() -> Result<()> { env_logger::init(); let cli = Cli::parse(); @@ -66,9 +67,10 @@ pub fn main() -> Result<()> { initial: deckster_file.initial, active_button_color: deckster_file.active_button_color, inactive_button_color: deckster_file.inactive_button_color, - }; + } + .validate()?; - runner::start(config)? + runner::start(config).await? } }; diff --git a/deckster/src/model/config.rs b/deckster/src/model/config.rs index ad597fa..7549d6e 100644 --- a/deckster/src/model/config.rs +++ b/deckster/src/model/config.rs @@ -1,21 +1,22 @@ use std::collections::HashMap; +use color_eyre::{eyre::eyre, Result}; use enum_map::EnumMap; use rgb::RGB8; use serde::Deserialize; use crate::model; use crate::model::image_filter::ImageFilter; -use crate::model::rgb::DeserializableRGB8; +use crate::model::rgb::SerializableRGB8; use crate::model::ButtonPosition; #[derive(Debug, Deserialize)] pub struct File { pub icon_packs: Vec, #[serde(default = "inactive_button_color_default")] - pub inactive_button_color: DeserializableRGB8, + pub inactive_button_color: SerializableRGB8, #[serde(default = "active_button_color_default")] - pub active_button_color: DeserializableRGB8, + pub active_button_color: SerializableRGB8, pub buttons: HashMap, // EnumMap pub initial: InitialConfig, } @@ -31,18 +32,18 @@ pub struct Config { pub key_pages_by_id: HashMap, pub knob_pages_by_id: HashMap, pub icon_packs: Vec, - pub inactive_button_color: DeserializableRGB8, - pub active_button_color: DeserializableRGB8, + pub inactive_button_color: SerializableRGB8, + pub active_button_color: SerializableRGB8, pub buttons: EnumMap, pub initial: InitialConfig, } -fn inactive_button_color_default() -> DeserializableRGB8 { - DeserializableRGB8(RGB8::new(128, 128, 128)) +fn inactive_button_color_default() -> SerializableRGB8 { + SerializableRGB8(RGB8::new(128, 128, 128)) } -fn active_button_color_default() -> DeserializableRGB8 { - DeserializableRGB8(RGB8::new(0, 255, 0)) +fn active_button_color_default() -> SerializableRGB8 { + SerializableRGB8(RGB8::new(0, 255, 0)) } #[derive(Debug, Deserialize, Default)] @@ -63,3 +64,43 @@ pub struct IconPack { pub path: String, pub global_filter: Option, } + +impl Config { + pub fn validate(self) -> Result { + if !self.key_pages_by_id.contains_key(&self.initial.key_page) { + return Err(eyre!(format!( + "There is no key page with the ID specified at initial.key_page: {}", + &self.initial.key_page + ))); + } + + if !self.knob_pages_by_id.contains_key(&self.initial.knob_page) { + return Err(eyre!(format!( + "There is no knob page with the ID specified at initial.knob_page: {}", + &self.initial.knob_page + ))); + } + + for (position, button) in &self.buttons { + if let Some(key_page) = &button.key_page { + if !self.key_pages_by_id.contains_key(key_page) { + return Err(eyre!(format!( + "There is no key page with the ID specified at buttons.{}.key_page: {}", + position, &self.initial.key_page + ))); + } + } + + if let Some(knob_page) = &button.knob_page { + if !self.knob_pages_by_id.contains_key(knob_page) { + return Err(eyre!(format!( + "There is no knob page with the ID specified at buttons.{}.knob_page: {}", + position, &self.initial.knob_page + ))); + } + } + } + + Ok(self) + } +} diff --git a/deckster/src/model/geometry.rs b/deckster/src/model/geometry.rs index 7119cd5..85858c3 100644 --- a/deckster/src/model/geometry.rs +++ b/deckster/src/model/geometry.rs @@ -5,7 +5,7 @@ use piet::kurbo::{Rect, Vec2}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -#[derive(Debug, SerializeDisplay, DeserializeFromStr, Eq, PartialEq, Hash)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)] pub struct UIntVec2 { x: u64, y: u64, @@ -63,3 +63,7 @@ pub fn parse_positive_rect_from_str(s: &str) -> Result { Rect::from_origin_size(first_vec.to_point(), second_vec.to_size()) }) } + +pub fn fmt_positive_rect(rect: &Rect, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}x{}-{}x{}", rect.x0, rect.x1, rect.y0, rect.y1)) +} diff --git a/deckster/src/model/icon_descriptor.rs b/deckster/src/model/icon_descriptor.rs index 3b4202b..be16b73 100644 --- a/deckster/src/model/icon_descriptor.rs +++ b/deckster/src/model/icon_descriptor.rs @@ -1,12 +1,16 @@ use std::path::PathBuf; use std::str::FromStr; +use serde::{Deserialize, Serialize}; use serde_with::DeserializeFromStr; use thiserror::Error; use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; -#[derive(Debug, Default, Clone, DeserializeFromStr)] +#[derive(Debug, Default, PartialEq, Clone, DeserializeFromStr)] +pub struct IconDescriptorString(pub IconDescriptor); + +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct IconDescriptor { pub source: IconDescriptorSource, pub filter: Option, @@ -24,7 +28,7 @@ pub enum IconDescriptorFromStrError { MissingImageFilterClosingBracket, } -impl FromStr for IconDescriptor { +impl FromStr for IconDescriptorString { type Err = IconDescriptorFromStrError; fn from_str(s: &str) -> Result { @@ -53,11 +57,11 @@ impl FromStr for IconDescriptor { Some(ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?) }; - Ok(IconDescriptor { source, filter }) + Ok(IconDescriptorString(IconDescriptor { source, filter })) } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)] pub enum IconDescriptorSource { #[default] None, diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 1960eb5..b8af4fc 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -1,14 +1,15 @@ +use std::fmt::{Display, Formatter}; use std::str::FromStr; use piet::kurbo::Rect; use rgb::RGB8; -use serde_with::DeserializeFromStr; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use crate::model::geometry::parse_positive_rect_from_str; -use crate::model::rgb::parse_rgb8_from_hex_str; +use crate::model::geometry::{fmt_positive_rect, parse_positive_rect_from_str}; +use crate::model::rgb::{fmt_rgb8_as_hex_string, parse_rgb8_from_hex_str}; -#[derive(Debug, Clone, DeserializeFromStr)] +#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)] pub struct ImageFilter { pub crop_original: Option, // applied before scale and rotate pub scale: f32, @@ -21,6 +22,24 @@ pub struct ImageFilter { pub invert: bool, } +const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter { + crop_original: None, + scale: 1.0, + clockwise_quarter_rotations: 0, + crop: None, + color: None, + alpha: 1.0, + blur: 0.0, + grayscale: false, + invert: false, +}; + +impl Default for ImageFilter { + fn default() -> Self { + DEFAULT_IMAGE_FILTER.clone() + } +} + #[derive(Debug, Error)] pub enum ImageFilterFromStringError { #[error("Unknown filter: {name}")] @@ -80,18 +99,7 @@ impl FromStr for ImageFilter { let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect(); - let mut result = ImageFilter { - crop_original: None, - scale: 1.0, - clockwise_quarter_rotations: 0, - crop: None, - color: None, - alpha: 1.0, - blur: 0.0, - grayscale: false, - invert: false, - }; - + let mut result = ImageFilter::default(); let mut previous_filter_names: Vec = Vec::new(); for filter in filters { @@ -165,3 +173,89 @@ impl FromStr for ImageFilter { Ok(result) } } + +impl Display for ImageFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut is_first = true; + if let Some(rect) = self.crop_original { + if !is_first { + f.write_str("|")? + } + f.write_str("crop_original=")?; + fmt_positive_rect(&rect, f)?; + is_first = false; + } + + if self.scale != DEFAULT_IMAGE_FILTER.scale { + if !is_first { + f.write_str("|")? + } + f.write_str("scale=")?; + self.scale.fmt(f)?; + is_first = false; + } + + if self.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.clockwise_quarter_rotations { + if !is_first { + f.write_str("|")? + } + f.write_str("rotate=")?; + (self.clockwise_quarter_rotations * 90).fmt(f)?; + is_first = false; + } + + if let Some(rect) = self.crop { + if !is_first { + f.write_str("|")? + } + f.write_str("crop=")?; + fmt_positive_rect(&rect, f)?; + is_first = false; + } + + if let Some(color) = self.color { + if !is_first { + f.write_str("|")? + } + f.write_str("color=")?; + fmt_rgb8_as_hex_string(&color, f)?; + is_first = false; + } + + if self.alpha != DEFAULT_IMAGE_FILTER.alpha { + if !is_first { + f.write_str("|")? + } + f.write_str("alpha=")?; + self.alpha.fmt(f)?; + is_first = false; + } + + if self.blur != DEFAULT_IMAGE_FILTER.blur { + if !is_first { + f.write_str("|")? + } + f.write_str("blur=")?; + self.blur.fmt(f)?; + is_first = false; + } + + if self.grayscale { + if !is_first { + f.write_str("|")? + } + f.write_str("grayscale")?; + is_first = false; + } + + if self.invert { + if !is_first { + f.write_str("|")? + } + f.write_str("invert")?; + // is_first = false; + } + + Ok(()) + } +} diff --git a/deckster/src/model/key_modes/home_assistant.rs b/deckster/src/model/key_modes/home_assistant.rs index 6d71a2b..6ff1ae8 100644 --- a/deckster/src/model/key_modes/home_assistant.rs +++ b/deckster/src/model/key_modes/home_assistant.rs @@ -2,20 +2,20 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::icon_descriptor::IconDescriptorString; #[derive(Debug, Deserialize)] pub struct SwitchConfig { pub name: String, #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, } #[derive(Debug, Deserialize)] pub struct ButtonConfig { pub name: String, #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_modes/media.rs b/deckster/src/model/key_modes/media.rs index fe9ac1b..05ad60f 100644 --- a/deckster/src/model/key_modes/media.rs +++ b/deckster/src/model/key_modes/media.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::icon_descriptor::IconDescriptorString; #[derive(Debug, Deserialize)] pub struct PlayPauseConfig { #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, #[serde(default)] pub action: PlayPauseAction, } @@ -24,7 +24,7 @@ pub enum PlayPauseAction { #[derive(Debug, Deserialize)] pub struct PreviousAndNextConfig { #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_modes/spotify.rs b/deckster/src/model/key_modes/spotify.rs index 612a424..a6e30b4 100644 --- a/deckster/src/model/key_modes/spotify.rs +++ b/deckster/src/model/key_modes/spotify.rs @@ -2,18 +2,18 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::icon_descriptor::IconDescriptorString; #[derive(Debug, Deserialize)] pub struct ShuffleConfig { #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, } #[derive(Debug, Deserialize)] pub struct RepeatConfig { #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index b092783..d5d0379 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -3,21 +3,21 @@ use std::collections::HashMap; use serde::Deserialize; use crate::model::geometry::UIntVec2; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::{key_modes, KnobPosition}; +use crate::model::icon_descriptor::IconDescriptorString; +use crate::model::{key_modes, KeyPosition, KnobPosition}; #[derive(Debug, Deserialize)] pub struct File { pub id: Option, pub scrolling: Option, - pub keys: HashMap, + pub keys: HashMap, } #[derive(Debug)] pub struct Page { pub id: String, pub scrolling: Option, - pub keys: HashMap, + pub keys: HashMap, } #[derive(Debug, Deserialize)] @@ -46,7 +46,7 @@ pub enum ScrollingConfigAxis { #[derive(Debug, Deserialize)] pub struct KeyConfig { pub label: Option, - pub icon: Option, + pub icon: Option, #[serde(default)] pub mode: KeyModes, } diff --git a/deckster/src/model/knob_modes/audio_volume.rs b/deckster/src/model/knob_modes/audio_volume.rs index 07f2d1f..7291981 100644 --- a/deckster/src/model/knob_modes/audio_volume.rs +++ b/deckster/src/model/knob_modes/audio_volume.rs @@ -4,8 +4,8 @@ use std::num::NonZeroU8; use regex::Regex; use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::rgb::DeserializableRGB8WithOptionalAlpha; +use crate::model::icon_descriptor::IconDescriptorString; +use crate::model::rgb::SerializableRGB8WithOptionalAlpha; #[derive(Debug, Deserialize)] pub struct Config { @@ -22,7 +22,7 @@ pub struct Config { #[serde(default)] pub label: HashMap, #[serde(default)] - pub icon: HashMap, + pub icon: HashMap, pub circle_indicator: Option, pub bar_indicator: Option, } @@ -54,12 +54,12 @@ pub enum State { #[derive(Debug, Deserialize)] pub struct CircleIndicatorConfig { - pub color: DeserializableRGB8WithOptionalAlpha, + pub color: SerializableRGB8WithOptionalAlpha, pub width: NonZeroU8, pub radius: u8, } #[derive(Debug, Deserialize)] pub struct BarIndicatorConfig { - pub color: DeserializableRGB8WithOptionalAlpha, + pub color: SerializableRGB8WithOptionalAlpha, } diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index c7f84b2..7104e65 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use enum_map::EnumMap; use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::rgb::DeserializableRGB8WithOptionalAlpha; +use crate::model::icon_descriptor::IconDescriptorString; +use crate::model::rgb::SerializableRGB8WithOptionalAlpha; use crate::model::{knob_modes, KnobPosition}; #[derive(Debug, Deserialize)] @@ -24,7 +24,7 @@ pub struct Knob { #[serde(default)] pub label: String, #[serde(default)] - pub icon: IconDescriptor, + pub icon: IconDescriptorString, #[serde(default)] pub indicator: KnobIndicators, #[serde(default)] @@ -39,12 +39,12 @@ pub struct KnobIndicators { #[derive(Debug, Deserialize)] pub struct KnobIndicatorBarConfig { - pub color: DeserializableRGB8WithOptionalAlpha, + pub color: SerializableRGB8WithOptionalAlpha, } #[derive(Debug, Deserialize)] pub struct KnobIndicatorCircleConfig { - pub color: DeserializableRGB8WithOptionalAlpha, + pub color: SerializableRGB8WithOptionalAlpha, pub width: u8, pub radius: u8, } diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index fee147c..7a3fca1 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -1,5 +1,12 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + use enum_map::Enum; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use thiserror::Error; + +use loupedeck_serial::characteristics::LoupedeckButton; pub mod config; pub mod geometry; @@ -11,7 +18,41 @@ pub mod knob_modes; pub mod knob_page; pub mod rgb; -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] +pub struct KeyPosition { + x: u16, + y: u16, +} + +#[derive(Debug, Error)] +#[error("The input value does not match the required format of and separated by an 'x'")] +pub struct KeyPositionFromStrError {} + +impl FromStr for KeyPosition { + type Err = KeyPositionFromStrError; + + fn from_str(s: &str) -> Result { + let values = s.split_once('x'); + + if let Some((x, y)) = values { + if let Ok(x) = u16::from_str(x) { + if let Ok(y) = u16::from_str(y) { + return Ok(KeyPosition { x, y }); + } + } + } + + Err(KeyPositionFromStrError {}) + } +} + +impl Display for KeyPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}x{}", self.x, self.y)) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)] #[serde(rename_all = "kebab-case")] pub enum KnobPosition { LeftTop, @@ -41,3 +82,33 @@ pub enum ButtonPosition { #[serde(rename = "7")] N7, } + +impl ButtonPosition { + pub fn of(button: &LoupedeckButton) -> Self { + match button { + LoupedeckButton::N0 => Self::N0, + LoupedeckButton::N1 => Self::N1, + LoupedeckButton::N2 => Self::N2, + LoupedeckButton::N3 => Self::N3, + LoupedeckButton::N4 => Self::N4, + LoupedeckButton::N5 => Self::N5, + LoupedeckButton::N6 => Self::N6, + LoupedeckButton::N7 => Self::N7, + } + } +} + +impl Display for ButtonPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ButtonPosition::N0 => "0", + ButtonPosition::N1 => "1", + ButtonPosition::N2 => "2", + ButtonPosition::N3 => "3", + ButtonPosition::N4 => "4", + ButtonPosition::N5 => "5", + ButtonPosition::N6 => "6", + ButtonPosition::N7 => "7", + }) + } +} diff --git a/deckster/src/model/rgb.rs b/deckster/src/model/rgb.rs index 40c5f14..c4c2cca 100644 --- a/deckster/src/model/rgb.rs +++ b/deckster/src/model/rgb.rs @@ -1,7 +1,8 @@ +use std::fmt::{Display, Formatter}; use std::str::FromStr; use rgb::{RGB8, RGBA8}; -use serde_with::DeserializeFromStr; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; #[derive(Debug, Error)] @@ -21,6 +22,10 @@ pub fn parse_rgb8_from_hex_str(s: &str) -> Result { Err(RGBParsingError {}) } +pub fn fmt_rgb8_as_hex_string(v: &RGB8, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:#04x}{:#04x}{:#04x}", v.r, v.g, v.b)) +} + pub fn parse_rgba8_from_hex_str(s: &str) -> Result { let first_index = if s.starts_with('#') { 1 } else { 0 }; if s.len() - first_index == 8 { @@ -35,6 +40,10 @@ pub fn parse_rgba8_from_hex_str(s: &str) -> Result { Err(RGBParsingError {}) } +pub fn fmt_rgba8_as_hex_string(v: &RGBA8, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:#04x}{:#04x}{:#04x}{:#04x}", v.r, v.g, v.b, v.a)) +} + pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result { // optionally +1 for the '#' match s.len() { @@ -44,35 +53,53 @@ pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) } } -#[derive(Debug, DeserializeFromStr)] -pub struct DeserializableRGB8(pub RGB8); +#[derive(Debug, SerializeDisplay, DeserializeFromStr)] +pub struct SerializableRGB8(pub RGB8); -impl FromStr for DeserializableRGB8 { +impl FromStr for SerializableRGB8 { type Err = RGBParsingError; fn from_str(s: &str) -> Result { - parse_rgb8_from_hex_str(s).map(|v| DeserializableRGB8(v)) + parse_rgb8_from_hex_str(s).map(|v| SerializableRGB8(v)) } } -#[derive(Debug, DeserializeFromStr)] -pub struct DeserializableRGBA8(pub RGBA8); +impl Display for SerializableRGB8 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fmt_rgb8_as_hex_string(&self.0, f) + } +} -impl FromStr for DeserializableRGBA8 { +#[derive(Debug, SerializeDisplay, DeserializeFromStr)] +pub struct SerializableRGBA8(pub RGBA8); + +impl FromStr for SerializableRGBA8 { type Err = RGBParsingError; fn from_str(s: &str) -> Result { - parse_rgba8_from_hex_str(s).map(|v| DeserializableRGBA8(v)) + parse_rgba8_from_hex_str(s).map(|v| SerializableRGBA8(v)) } } -#[derive(Debug, DeserializeFromStr)] -pub struct DeserializableRGB8WithOptionalAlpha(pub RGBA8); +impl Display for SerializableRGBA8 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fmt_rgba8_as_hex_string(&self.0, f) + } +} -impl FromStr for DeserializableRGB8WithOptionalAlpha { +#[derive(Debug, SerializeDisplay, DeserializeFromStr)] +pub struct SerializableRGB8WithOptionalAlpha(pub RGBA8); + +impl FromStr for SerializableRGB8WithOptionalAlpha { type Err = RGBParsingError; fn from_str(s: &str) -> Result { - parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(|v| DeserializableRGB8WithOptionalAlpha(v)) + parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(|v| SerializableRGB8WithOptionalAlpha(v)) + } +} + +impl Display for SerializableRGB8WithOptionalAlpha { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fmt_rgba8_as_hex_string(&self.0, f) } } diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs deleted file mode 100644 index 8d12efe..0000000 --- a/deckster/src/runner/command.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub enum RendererCommand { - SetButton, -} diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 301ba55..3a29710 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -1,21 +1,29 @@ use std::collections::HashMap; -use std::rc::Rc; +use std::sync::Arc; +use std::thread; use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; +use enum_map::EnumMap; +use enum_ordinalize::Ordinalize; +use flume::{Receiver, Sender}; +use log::{debug, info, trace}; +use rgb::RGB8; +use loupedeck_serial::characteristics::LoupedeckButton; use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; -use state::State; +use loupedeck_serial::events::LoupedeckEvent; use crate::model; +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::ButtonPosition; +use crate::runner::state::{State, StateChangeCommand}; -mod command; mod state; -pub fn start(config: model::config::Config) -> Result<()> { - let state = create_state(&config)?; - +pub async fn start(config: model::config::Config) -> Result<()> { + let config = Arc::new(config); let device = LoupedeckDevice::discover()? .first() .wrap_err("No device connected.")? @@ -24,15 +32,41 @@ pub fn start(config: model::config::Config) -> Result<()> { device.vibrate(VibrationPattern::RiseFall); + let events_receiver = device.events(); + let (commands_sender, commands_receiver) = flume::bounded::(20); + + let cloned_config = Arc::clone(&config); + let cloned_commands_sender = commands_sender.clone(); + let io_worker_thread = thread::Builder::new() + .name("deckster IO worker".to_owned()) + .spawn(move || do_io_work(cloned_config, device, events_receiver, cloned_commands_sender, commands_receiver)) + .wrap_err("Could not spawn the worker thread")?; + + commands_sender.send(StateChangeCommand::RefreshButtonColors).unwrap(); + + io_worker_thread.join().unwrap(); + Ok(()) } -fn create_state(config: &model::config::Config) -> Result { +fn create_state(config: &model::config::Config) -> State { let key_pages_by_id: HashMap<_, _> = config .key_pages_by_id .iter() - .map(|(id, p)| state::KeyPage { id: id.clone() }) - .map(|p| (p.id.clone(), Rc::new(p))) + .map(|(id, p)| state::KeyPage { + id: id.clone(), + keys_by_position: p + .keys + .iter() + .map(|(position, k)| state::Key { + position: *position, + label: k.label.clone().unwrap_or_default(), + icon: IconDescriptor::default(), + }) + .map(|k| (k.position, k)) + .collect(), + }) + .map(|p| (p.id.clone(), p)) .collect(); let knob_pages_by_id: HashMap<_, _> = config @@ -40,33 +74,143 @@ fn create_state(config: &model::config::Config) -> Result { .iter() .map(|(id, p)| state::KnobPage { id: id.clone(), - knobs_by_position: p - .knobs - .iter() - .map(|(p, k)| state::Knob { - id: p, - label: k.label.clone(), - icon: k.icon.clone(), + knobs_by_position: EnumMap::from_fn(|position| { + let knob_config = &p.knobs[position]; + + state::Knob { + position, + label: knob_config.label.clone(), + icon: knob_config.icon.0.clone(), value: 0.0, - }) - .map(|k| (k.id, Some(k))) - .collect(), + } + }), }) - .map(|p| (p.id.clone(), Rc::new(p))) + .map(|p| (p.id.clone(), p)) .collect(); - Ok(State { - active_key_page: Rc::clone( - key_pages_by_id - .get(&config.initial.key_page) - .wrap_err_with(|| format!("There is no key page with the ID specified at initial.key_page: {}", &config.initial.key_page))?, - ), - active_knob_page: Rc::clone( - knob_pages_by_id - .get(&config.initial.knob_page) - .wrap_err_with(|| format!("There is no key page with the ID specified at initial.knob_page: {}", &config.initial.key_page))?, - ), + State { + active_key_page_id: config.initial.key_page.clone(), + active_knob_page_id: config.initial.knob_page.clone(), key_pages_by_id, knob_pages_by_id, - }) + } +} + +enum IoWork { + Event(LoupedeckEvent), + Command(StateChangeCommand), +} + +fn do_io_work( + config: Arc, + device: LoupedeckDevice, + events_receiver: Receiver, + commands_sender: Sender, + commands_receiver: Receiver, +) { + let mut state = create_state(&config); + + loop { + let a = flume::Selector::new() + .recv(&events_receiver, |e| IoWork::Event(e.unwrap())) + .recv(&commands_receiver, |c| IoWork::Command(c.unwrap())) + .wait(); + + match a { + IoWork::Event(event) => { + if !handle_event(&config, &mut state, &commands_sender, event) { + break; + } + } + IoWork::Command(command) => handle_command(&config, &mut state, &device, command), + } + } +} + +fn handle_event(config: &model::config::Config, state: &mut State, commands_sender: &Sender, event: LoupedeckEvent) -> bool { + trace!("Handling event: {:?}", &event); + + match event { + LoupedeckEvent::Disconnected => return false, + LoupedeckEvent::ButtonDown { button } => { + let position = ButtonPosition::of(&button); + let button_config = &config.buttons[position]; + let mut did_change = false; + + if let Some(key_page) = &button_config.key_page { + did_change = true; + info!("Switching to key page: {}", key_page); + + state.active_key_page_id = key_page.clone(); + } + + if let Some(knob_page) = &button_config.knob_page { + did_change = true; + info!("Switching to knob page: {}", knob_page); + state.active_knob_page_id = knob_page.clone(); + } + + if did_change { + commands_sender.send(StateChangeCommand::RefreshButtonColors).unwrap() + } + } + _ => {} + } + + true +} + +fn handle_command(config: &model::config::Config, state: &mut State, device: &LoupedeckDevice, command: StateChangeCommand) { + debug!("Handling command: {:?}", &command); + + match command { + StateChangeCommand::RefreshButtonColors => { + for button in LoupedeckButton::VARIANTS { + let position = ButtonPosition::of(button); + + device.set_button_color(*button, get_correct_button_color(config, state, position)).unwrap(); + } + } + StateChangeCommand::SetKeyLabel { page_id, key_position, value } => { + state.mutate_key_for_command( + "SetKeyLabel", + &page_id, + &key_position, + Box::new(|k| { + k.label = value; + }), + ); + } + StateChangeCommand::SetKeyIcon { page_id, key_position, value } => { + state.mutate_key_for_command( + "SetKeyIcon", + &page_id, + &key_position, + Box::new(|k| { + k.icon = value; + }), + ); + } + } +} + +// active -> config.active_button_color +// no actions defined -> #000000 +// inactive -> config.inactive_button_color +fn get_correct_button_color(config: &model::config::Config, state: &mut State, button_position: ButtonPosition) -> RGB8 { + let button_config = &config.buttons[button_position]; + + if let Some(key_page) = &button_config.key_page { + if key_page == &state.active_key_page_id { + if let Some(knob_page) = &button_config.knob_page { + if knob_page == &state.active_knob_page_id { + return config.active_button_color.0; + } + } + } + } else if button_config.knob_page.is_none() { + return RGB8::new(0, 0, 0); + } + + config.inactive_button_color.0 } diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs new file mode 100644 index 0000000..56d71e9 --- /dev/null +++ b/deckster/src/runner/state.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +use enum_map::EnumMap; +use log::error; +use serde::{Deserialize, Serialize}; + +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::{KeyPosition, KnobPosition}; + +#[derive(Debug)] +pub struct State { + pub active_key_page_id: String, + pub active_knob_page_id: String, + pub key_pages_by_id: HashMap, + pub knob_pages_by_id: HashMap, +} + +impl State { + pub fn mutate_key_for_command( + &mut self, + command_name: &'static str, + page_id: &String, + key_position: &KeyPosition, + mutator: Box R>, + ) -> Option { + match self.key_pages_by_id.get_mut(page_id) { + None => error!("Received {} command with invalid page_id: {}", command_name, page_id), + Some(key_page) => match key_page.keys_by_position.get_mut(&key_position) { + None => error!("Received {} command with invalid key_position: {}", command_name, key_position), + Some(key) => return Some(mutator(key)), + }, + } + + None + } +} + +#[derive(Debug)] +pub struct KeyPage { + pub id: String, + pub keys_by_position: HashMap, +} + +#[derive(Debug)] +pub struct KnobPage { + pub id: String, + pub knobs_by_position: EnumMap, +} + +#[derive(Debug)] +pub struct Key { + pub position: KeyPosition, + pub icon: IconDescriptor, + pub label: String, +} + +#[derive(Debug)] +pub struct Knob { + pub position: KnobPosition, + pub icon: IconDescriptor, + pub label: String, + pub value: f32, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum StateChangeCommand { + RefreshButtonColors, + SetKeyLabel { + page_id: String, + key_position: KeyPosition, + value: String, + }, + SetKeyIcon { + page_id: String, + key_position: KeyPosition, + value: IconDescriptor, + }, +} diff --git a/deckster/src/runner/state/mod.rs b/deckster/src/runner/state/mod.rs deleted file mode 100644 index 687768f..0000000 --- a/deckster/src/runner/state/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::collections::HashMap; -use std::rc::Rc; - -use enum_map::EnumMap; - -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::KnobPosition; - -#[derive(Debug)] -pub struct State { - pub active_key_page: Rc, - pub active_knob_page: Rc, - pub key_pages_by_id: HashMap>, - pub knob_pages_by_id: HashMap>, -} - -#[derive(Debug)] -pub struct KeyPage { - pub id: String, -} - -#[derive(Debug)] -pub struct KnobPage { - pub id: String, - pub knobs_by_position: EnumMap>, -} - -#[derive(Debug)] -pub struct Knob { - pub id: KnobPosition, - pub icon: IconDescriptor, - pub label: String, - pub value: f32, -} diff --git a/loupedeck_serial/Cargo.toml b/loupedeck_serial/Cargo.toml index 83c7e5f..fcbc3a3 100644 --- a/loupedeck_serial/Cargo.toml +++ b/loupedeck_serial/Cargo.toml @@ -9,5 +9,5 @@ enum-ordinalize = "4.3.0" enumset = "1.1.3" bytes = "1.5.0" thiserror = "1.0.52" -crossbeam-channel = "0.5.10" -rgb = "0.8.37" \ No newline at end of file +rgb = "0.8.37" +flume = "0.11.0" \ No newline at end of file diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index a4139ca..e8fe0de 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -6,23 +6,17 @@ use crate::util::Endianness; #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u8)] pub enum LoupedeckKnob { - KnobTopLeft = 0x01, - KnobCenterLeft = 0x02, - KnobBottomLeft = 0x03, - KnobTopRight = 0x04, - KnobCenterRight = 0x05, - KnobBottomRight = 0x06, + KnobLeftTop = 0x01, + KnobLeftMiddle = 0x02, + KnobLeftBottom = 0x03, + KnobRightTop = 0x04, + KnobRightMiddle = 0x05, + KnobRightBottom = 0x06, } #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u8)] pub enum LoupedeckButton { - KnobLeftTop = 0x01, - KnobLeftCenter = 0x02, - KnobLeftBottom = 0x03, - KnobRightTop = 0x04, - KnobRightCenter = 0x05, - KnobRightBottom = 0x06, N0 = 0x07, N1 = 0x08, N2 = 0x09, @@ -33,12 +27,6 @@ pub enum LoupedeckButton { N7 = 0x0e, } -impl LoupedeckButton { - pub fn supports_color(&self) -> bool { - self.ordinal() >= LoupedeckButton::N0.ordinal() && self.ordinal() <= LoupedeckButton::N7.ordinal() - } -} - #[derive(Debug)] #[non_exhaustive] pub struct LoupedeckDeviceDisplayConfiguration { @@ -117,21 +105,15 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck product_id: 0x0004, name: "Loupedeck Live", available_knobs: enum_set!( - LoupedeckKnob::KnobTopLeft - | LoupedeckKnob::KnobCenterLeft - | LoupedeckKnob::KnobBottomLeft - | LoupedeckKnob::KnobTopRight - | LoupedeckKnob::KnobCenterRight - | LoupedeckKnob::KnobBottomRight + LoupedeckKnob::KnobLeftTop + | LoupedeckKnob::KnobLeftMiddle + | LoupedeckKnob::KnobLeftBottom + | LoupedeckKnob::KnobRightTop + | LoupedeckKnob::KnobRightMiddle + | LoupedeckKnob::KnobRightBottom ), available_buttons: enum_set!( - LoupedeckButton::KnobLeftTop - | LoupedeckButton::KnobLeftCenter - | LoupedeckButton::KnobLeftBottom - | LoupedeckButton::KnobRightTop - | LoupedeckButton::KnobRightCenter - | LoupedeckButton::KnobRightBottom - | LoupedeckButton::N0 + LoupedeckButton::N0 | LoupedeckButton::N1 | LoupedeckButton::N2 | LoupedeckButton::N3 diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs index 6db8119..a8b27f8 100644 --- a/loupedeck_serial/src/device.rs +++ b/loupedeck_serial/src/device.rs @@ -1,18 +1,20 @@ -use crate::characteristics::{LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration, CHARACTERISTICS}; -use crate::commands::{LoupedeckCommand, VibrationPattern}; -use crate::events::{LoupedeckEvent, LoupedeckInternalEvent}; -use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START}; -use crate::util::convert_rgb888_to_rgb565; -use bytes::Bytes; -use rgb::{ComponentSlice, RGB8}; -use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use std::io::{Read, Write}; use std::sync::mpsc; use std::thread::sleep; use std::time::Duration; use std::{io, thread}; + +use bytes::Bytes; +use rgb::{ComponentSlice, RGB8}; +use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use thiserror::Error; +use crate::characteristics::{LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration, CHARACTERISTICS}; +use crate::commands::{LoupedeckCommand, VibrationPattern}; +use crate::events::{LoupedeckEvent, LoupedeckInternalEvent}; +use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START}; +use crate::util::convert_rgb888_to_rgb565; + #[derive(Debug)] pub struct AvailableLoupedeckDevice { pub(crate) port_name: String, @@ -38,8 +40,8 @@ pub struct LoupedeckDevice { pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, pub(crate) serial_number: String, pub(crate) firmware_version: String, - events_receiver: crossbeam_channel::Receiver, - commands_sender: crossbeam_channel::Sender, + events_receiver: flume::Receiver, + commands_sender: flume::Sender, } #[derive(Debug, Error)] @@ -70,9 +72,6 @@ pub enum RefreshDisplayError { pub enum SetButtonColorError { #[error("The specified button is not available for this device.")] UnknownButton, - - #[error("The button does not allow setting a color.")] - ColorNotSupported, } #[derive(Debug, Error)] @@ -100,7 +99,7 @@ impl LoupedeckDevice { &self.firmware_version } - pub fn events_channel(&self) -> crossbeam_channel::Receiver { + pub fn events(&self) -> flume::Receiver { self.events_receiver.clone() } @@ -113,10 +112,6 @@ impl LoupedeckDevice { return Err(SetButtonColorError::UnknownButton); } - if !button.supports_color() { - return Err(SetButtonColorError::ColorNotSupported); - } - // The write worker thread not running means the device was disconnected. // In that case, the read worker thread sends a LoupedeckEvent::Disconnected. self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).ok(); @@ -220,50 +215,46 @@ impl LoupedeckDevice { } pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result { - let mut port = serialport::new(port_name, 256000) - .data_bits(DataBits::Eight) - .stop_bits(StopBits::One) - .parity(Parity::None) - .flow_control(FlowControl::None) - .timeout(Duration::from_secs(10)) - .open()?; - - port.write_all(WS_UPGRADE_REQUEST.as_bytes())?; - port.flush()?; + let mut port = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?; let mut buf = [0; WS_UPGRADE_RESPONSE_START.len()]; - port.read_exact(&mut buf)?; + + if port.read_exact(&mut buf).is_err() { + drop(port); + port = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?; + port.read_exact(&mut buf).unwrap(); + } if buf != WS_UPGRADE_RESPONSE_START.as_bytes() { return Err(ConnectError::WrongEarlyHandshakeResponse); } - // I don’t know why. There is garbage in the buffer without this. + // I don’t know why, but there is garbage in the buffer without this. sleep(Duration::from_secs(1)); port.clear(ClearBuffer::Input)?; let cloned_port = port.try_clone().expect("port must be cloneable"); let thread_name_base = format!("loupedeck_serial ({})", port.name().unwrap_or("".to_owned())); - let (public_events_sender, public_events_receiver) = crossbeam_channel::unbounded::(); + let (public_events_sender, public_events_receiver) = flume::unbounded::(); let (internal_events_sender, internal_events_receiver) = mpsc::sync_channel(2); thread::Builder::new().name(thread_name_base.to_owned() + " read worker").spawn(move || { read_messages_worker(port, public_events_sender, internal_events_sender); })?; - let (commands_sender, commands_receiver) = crossbeam_channel::unbounded::(); + let (commands_sender, commands_receiver) = flume::unbounded::(); thread::Builder::new().name(thread_name_base.to_owned() + " write worker").spawn(move || { write_messages_worker(cloned_port, commands_receiver); })?; commands_sender.send(LoupedeckCommand::RequestSerialNumber).unwrap(); - let serial_number = match internal_events_receiver.recv_timeout(Duration::from_secs(1)) { + let serial_number = match internal_events_receiver.recv_timeout(Duration::from_secs(10)) { Ok(LoupedeckInternalEvent::GetSerialNumberResponse { serial_number }) => Ok(serial_number), _ => Err(ConnectError::WrongLateHandshakeResponse), }?; commands_sender.send(LoupedeckCommand::RequestFirmwareVersion).unwrap(); - let firmware_version = match internal_events_receiver.recv_timeout(Duration::from_secs(1)) { + let firmware_version = match internal_events_receiver.recv_timeout(Duration::from_secs(10)) { Ok(LoupedeckInternalEvent::GetFirmwareVersionResponse { firmware_version }) => Ok(firmware_version), _ => Err(ConnectError::WrongLateHandshakeResponse), }?; @@ -278,4 +269,20 @@ impl LoupedeckDevice { commands_sender, }) } + + fn create_port_and_send_ws_upgrade_request(port_name: &str) -> serialport::Result> { + let mut port = serialport::new(port_name, 256000) + .data_bits(DataBits::Eight) + .stop_bits(StopBits::One) + .parity(Parity::None) + .flow_control(FlowControl::Software) + .timeout(Duration::from_secs(1)) + .open()?; + + port.clear(ClearBuffer::All).unwrap(); + port.write_all(WS_UPGRADE_REQUEST.as_bytes()).unwrap(); + port.flush().unwrap(); + + Ok(port) + } } diff --git a/loupedeck_serial/src/events.rs b/loupedeck_serial/src/events.rs index 563e5e3..b81af99 100644 --- a/loupedeck_serial/src/events.rs +++ b/loupedeck_serial/src/events.rs @@ -17,6 +17,8 @@ pub enum LoupedeckEvent { Disconnected, ButtonDown { button: LoupedeckButton }, ButtonUp { button: LoupedeckButton }, + KnobDown { knob: LoupedeckKnob }, + KnobUp { knob: LoupedeckKnob }, KnobRotate { knob: LoupedeckKnob, direction: RotationDirection }, Touch { touch_id: u8, x: u16, y: u16, is_end: bool }, } diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs index eb27aa1..68e024e 100644 --- a/loupedeck_serial/src/messages.rs +++ b/loupedeck_serial/src/messages.rs @@ -48,7 +48,7 @@ impl From for ParseMessageResult { pub(crate) fn read_messages_worker( mut port: Box, - public_sender: crossbeam_channel::Sender, + public_sender: flume::Sender, internal_sender: mpsc::SyncSender, ) { let mut internal_sender = Some(internal_sender); @@ -58,14 +58,23 @@ pub(crate) fn read_messages_worker( while !should_stop { let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH); - let read_length = port.read(&mut chunk).unwrap_or(0); + let read_result = port.read(&mut chunk); - if read_length == 0 { - // This fails only if the other side is disconnected. - // In that case, this thread should terminate anyway and we can ignore the error. - public_sender.send(LoupedeckEvent::Disconnected).ok(); - break; - } + let read_length = match read_result { + Ok(length) => length, + Err(err) => { + match err.kind() { + ErrorKind::BrokenPipe => { + // This fails only if the other side is disconnected. + // In that case, this thread should terminate anyway and we can ignore the error. + public_sender.send(LoupedeckEvent::Disconnected).ok(); + break; + } + ErrorKind::TimedOut => continue, + _ => panic!("{}", err), + } + } + }; chunk.truncate(read_length); buffer.put(chunk); @@ -120,18 +129,68 @@ pub(crate) fn read_messages_worker( fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { match command { - 0x00 => { - // Button - let button = LoupedeckButton::from_ordinal(message[0]).expect("Invalid button ID"); - - match message[1] { - 0x00 => LoupedeckEvent::ButtonDown { button }, - _ => LoupedeckEvent::ButtonUp { button }, - } - .into() + 0x00 => match message[1] { + 0x00 => match message[0] { + 0x01 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobLeftTop, + }, + 0x02 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobLeftMiddle, + }, + 0x03 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobLeftBottom, + }, + 0x04 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobRightTop, + }, + 0x05 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobRightMiddle, + }, + 0x06 => LoupedeckEvent::KnobDown { + knob: LoupedeckKnob::KnobRightBottom, + }, + 0x07 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N0 }, + 0x08 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N1 }, + 0x09 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N2 }, + 0x0a => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N3 }, + 0x0b => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N4 }, + 0x0c => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N5 }, + 0x0d => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N6 }, + 0x0e => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N7 }, + _ => panic!("Illegal button id: {}", message[1]), + }, + _ => match message[0] { + 0x01 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobLeftTop, + }, + 0x02 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobLeftMiddle, + }, + 0x03 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobLeftBottom, + }, + 0x04 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobRightTop, + }, + 0x05 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobRightMiddle, + }, + 0x06 => LoupedeckEvent::KnobUp { + knob: LoupedeckKnob::KnobRightBottom, + }, + 0x07 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N0 }, + 0x08 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N1 }, + 0x09 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N2 }, + 0x0a => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N3 }, + 0x0b => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N4 }, + 0x0c => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N5 }, + 0x0d => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N6 }, + 0x0e => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N7 }, + _ => panic!("Illegal button id: {}", message[1]), + }, } + .into(), 0x01 => { - // Knob let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID"); LoupedeckEvent::KnobRotate { @@ -149,7 +208,6 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { } .into(), 0x4d | 0x6d => { - // Touch message.advance(1); let x = message.get_u16(); let y = message.get_u16(); @@ -167,7 +225,7 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { } } -pub(crate) fn write_messages_worker(mut port: Box, receiver: crossbeam_channel::Receiver) { +pub(crate) fn write_messages_worker(mut port: Box, receiver: flume::Receiver) { let mut next_transaction_id = 0; let mut send = |command_id: u8, data: Bytes| -> Result<(), io::Error> {