From 634a1aa54777ca0e423049df08e657ed64e39b39 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Sun, 31 Dec 2023 00:59:27 +0100 Subject: [PATCH] Add model for whole example --- Cargo.lock | 2 +- deckster/Cargo.toml | 5 +- deckster/examples/full/button-page.toml | 17 ----- deckster/examples/full/deckster.toml | 19 ++++- deckster/examples/full/key-pages/default.toml | 35 +++++---- deckster/examples/full/key-pages/emojis.toml | 22 ++++-- deckster/examples/full/key-pages/numpad.toml | 36 ++++----- .../full/key-pages/special_chars.toml | 24 +++--- .../examples/full/knob-pages/default.toml | 8 +- deckster/src/main.rs | 38 ++++++---- deckster/src/model/deckster_file.rs | 25 ------ deckster/src/model/files/deckster.rs | 45 +++++++++++ deckster/src/model/files/key_page.rs | 56 ++++++++++++++ deckster/src/model/files/knob_page.rs | 22 ++++++ deckster/src/model/files/mod.rs | 3 + deckster/src/model/geometry.rs | 49 +++++++++--- deckster/src/model/icon_descriptor.rs | 64 ++++++++++++++++ deckster/src/model/image_filter.rs | 28 +++---- .../src/model/key_modes/home_assistant.rs | 34 +++++++++ deckster/src/model/key_modes/media.rs | 43 +++++++++++ deckster/src/model/key_modes/mod.rs | 5 ++ deckster/src/model/key_modes/spotify.rs | 32 ++++++++ deckster/src/model/key_modes/timer.rs | 15 ++++ deckster/src/model/key_modes/vibrate.rs | 39 ++++++++++ deckster/src/model/knob_modes/audio_volume.rs | 65 ++++++++++++++++ deckster/src/model/knob_modes/mod.rs | 1 + deckster/src/model/mod.rs | 22 +++++- deckster/src/model/rgb.rs | 69 +++++++++++++---- loupedeck_serial/src/characteristics.rs | 75 ++++++++++-------- loupedeck_serial/src/commands.rs | 5 +- loupedeck_serial/src/events.rs | 32 ++------ loupedeck_serial/src/lib.rs | 4 +- loupedeck_serial/src/messages.rs | 76 ++++++++----------- loupedeck_serial/src/util.rs | 4 +- 34 files changed, 756 insertions(+), 263 deletions(-) delete mode 100644 deckster/examples/full/button-page.toml delete mode 100644 deckster/src/model/deckster_file.rs create mode 100644 deckster/src/model/files/deckster.rs create mode 100644 deckster/src/model/files/key_page.rs create mode 100644 deckster/src/model/files/knob_page.rs create mode 100644 deckster/src/model/files/mod.rs create mode 100644 deckster/src/model/icon_descriptor.rs create mode 100644 deckster/src/model/key_modes/home_assistant.rs create mode 100644 deckster/src/model/key_modes/media.rs create mode 100644 deckster/src/model/key_modes/mod.rs create mode 100644 deckster/src/model/key_modes/spotify.rs create mode 100644 deckster/src/model/key_modes/timer.rs create mode 100644 deckster/src/model/key_modes/vibrate.rs create mode 100644 deckster/src/model/knob_modes/audio_volume.rs create mode 100644 deckster/src/model/knob_modes/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 229b08f..6bff92a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ dependencies = [ "humantime-serde", "loupedeck_serial", "piet", + "regex", "rgb", "serde", "serde_regex", @@ -643,7 +644,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" dependencies = [ "bytemuck", - "serde", ] [[package]] diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index c2a4517..f851d8a 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -8,9 +8,10 @@ color-eyre = "0.6.2" humantime-serde = "1.1.1" loupedeck_serial = { path = "../loupedeck_serial" } piet = "0.6.2" -rgb = { version = "0.8.37", features = ["serde"] } +rgb = "0.8.37" serde = { version = "1.0.193", features = ["derive"] } serde_regex = "1.1.0" serde_with = "3.4.0" thiserror = "1.0.52" -toml = "0.8.8" \ No newline at end of file +toml = "0.8.8" +regex = "1.10.2" \ No newline at end of file diff --git a/deckster/examples/full/button-page.toml b/deckster/examples/full/button-page.toml deleted file mode 100644 index 53409c2..0000000 --- a/deckster/examples/full/button-page.toml +++ /dev/null @@ -1,17 +0,0 @@ -id = "default" - -[defaults] -inactive_color = "#ff0000" -active_color = "#ffffff" - -[buttons.0] -mode.switch_page = { page = "default" } - -[buttons.1] -mode.switch_page = { page = "numpad" } - -[buttons.2] -mode.switch_page = { page = "emojis" } - -[buttons.3] -mode.switch_page = { page = "special_chars" } \ No newline at end of file diff --git a/deckster/examples/full/deckster.toml b/deckster/examples/full/deckster.toml index 8beee1c..72c48f8 100644 --- a/deckster/examples/full/deckster.toml +++ b/deckster/examples/full/deckster.toml @@ -1,10 +1,23 @@ key_pages = ["./key-pages/*"] -button_pages = ["./key-pages/*"] -knob_pages = ["./button-page.toml"] +knob_pages = ["./knob-pages/*"] + +inactive_button_color = "#ff0000" +active_button_color = "#ffffff" + +[buttons.0] +key_page = "default" + +[buttons.1] +key_page = "numpad" + +[buttons.2] +key_page = "emojis" + +[buttons.3] +key_page = "special_chars" [initial] key_page = "default" -button_page = "default" knob_page = "default" [[icon_packs]] diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index 2a90823..148f22c 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -1,28 +1,31 @@ -[defaults] -mode.vibrate = "low" +[keys.1x1] +icon = "@ph/play[alpha=0.8]" +mode.vibrate.pattern = "low" +mode.media__play_pause.icon.paused = "@ph/play" +mode.media__play_pause.icon.playing = "@ph/pause" -[keys.1-1] -mode.media__play_pause.icon.play = "@ph/play" -mode.media__play_pause.icon.pause = "@ph/pause" - -[keys.1-2] +[keys.1x2] icon = "@fad/shuffle" -mode.spotify__shuffle.border_color.is_on = "#58fc11" +mode.vibrate.pattern = "low" +mode.spotify__shuffle.icon.on = "@fad/shuffle[color=#58fc11]" -[keys.2-1] +[keys.2x1] icon = "@ph/timer" +mode.vibrate.pattern = "low" mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"] -mode.timer.vibrate_on_finish = true +mode.timer.vibrate_when_finished = true mode.timer.needy = true -[keys.3-3] +[keys.3x3] icon = "@fad/thunderbolt" label = "Dock" -mode.homeassistant__switch.name = "switch.moritz_thunderbolt_dock" -mode.homeassistant__switch.border_color.is_on = "#58fc11" +mode.vibrate.pattern = "low" +mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock" +mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]" -[keys.3-4] +[keys.3x4] icon = "@ph/computer-tower" label = "Tower PC" -mode.homeassistant__switch.name = "switch.mwin" -mode.homeassistant__switch.border_color.is_on = "#58fc11" \ No newline at end of file +mode.vibrate.pattern = "low" +mode.home_assistant__switch.name = "switch.mwin" +mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/emojis.toml b/deckster/examples/full/key-pages/emojis.toml index cd4bcc5..792a6e6 100644 --- a/deckster/examples/full/key-pages/emojis.toml +++ b/deckster/examples/full/key-pages/emojis.toml @@ -1,22 +1,28 @@ [scrolling] -knob = "bottom-right" -axis = "y" +knob = "right-bottom" +axis = "vertical" delta = 1 -[keys.4-1] +[keys.4x1] +label = "๐Ÿ˜€" mode.keyboard.key = "๐Ÿ˜€" -[keys.4-2] +[keys.4x2] +label = "๐Ÿ˜„" mode.keyboard.key = "๐Ÿ˜„" -[keys.4-3] +[keys.4x3] +label = "๐Ÿ˜…" mode.keyboard.key = "๐Ÿ˜…" -[keys.4-4] +[keys.4x4] +label = "๐Ÿ˜‚" mode.keyboard.key = "๐Ÿ˜‚" -[keys.4-5] +[keys.4x5] +label = "๐Ÿ˜" mode.keyboard.key = "๐Ÿ˜" -[keys.4-6] +[keys.4x6] +label = "๐Ÿ˜‡" mode.keyboard.key = "๐Ÿ˜‡" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/numpad.toml b/deckster/examples/full/key-pages/numpad.toml index 9422fd9..d9ce18f 100644 --- a/deckster/examples/full/key-pages/numpad.toml +++ b/deckster/examples/full/key-pages/numpad.toml @@ -1,35 +1,35 @@ -[keys.1-4] +[keys.1x4] label = "9" -mode.keyboard = { key = "9" } +mode.keyboard.key = "9" -[keys.1-3] +[keys.1x3] label = "8" -mode.keyboard = { key = "8" } +mode.keyboard.key = "8" -[keys.1-2] +[keys.1x2] label = "7" -mode.keyboard = { key = "7" } +mode.keyboard.key = "7" -[keys.2-4] +[keys.2x4] label = "6" -mode.keyboard = { key = "6" } +mode.keyboard.key = "6" -[keys.2-3] +[keys.2x3] label = "5" -mode.keyboard = { key = "5" } +mode.keyboard.key = "5" -[keys.2-2] +[keys.2x2] label = "4" -mode.keyboard = { key = "4" } +mode.keyboard.key = "4" -[keys.1-4] +[keys.1x4] label = "3" -mode.keyboard = { key = "3" } +mode.keyboard.key = "3" -[keys.1-3] +[keys.1x3] label = "2" -mode.keyboard = { key = "2" } +mode.keyboard.key = "2" -[keys.1-2] +[keys.1x2] label = "1" -mode.keyboard = { key = "1" } \ No newline at end of file +mode.keyboard.key = "1" \ No newline at end of file diff --git a/deckster/examples/full/key-pages/special_chars.toml b/deckster/examples/full/key-pages/special_chars.toml index 6510539..e0188f4 100644 --- a/deckster/examples/full/key-pages/special_chars.toml +++ b/deckster/examples/full/key-pages/special_chars.toml @@ -1,23 +1,29 @@ [scrolling] -button.previous = "3-3" -button.next = "4-3" -axis = "x" +button.previous = "3x3" +button.next = "4x3" +axis = "horizontal" delta = 4 -[keys.1-1] +[keys.1x1] +label = "โ€™" mode.keyboard.key = "โ€™" -[keys.2-1] +[keys.2x1] +label = "โ€ฆ" mode.keyboard.key = "โ€ฆ" -[keys.3-1] +[keys.3x1] +label = "โ€”" mode.keyboard.key = "โ€”" -[keys.4-1] +[keys.4x1] +label = "โ€“" mode.keyboard.key = "โ€“" -[keys.5-1] +[keys.5x1] +label = "ยท" mode.keyboard.key = "ยท" -[keys.6-1] +[keys.6x1] +label = "โ†’" mode.keyboard.key = "โ†’" \ No newline at end of file diff --git a/deckster/examples/full/knob-pages/default.toml b/deckster/examples/full/knob-pages/default.toml index 7a1bd2b..9965704 100644 --- a/deckster/examples/full/knob-pages/default.toml +++ b/deckster/examples/full/knob-pages/default.toml @@ -1,14 +1,14 @@ [knobs.left-top] icon = "@ph/microphone-light" -mode.audio_volume.mode = "input" +mode.audio_volume.direction = "input" mode.audio_volume.regex = "Microphone" mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@ph/microphone-sllash-light[alpha=90|brighten=50|huerotate=red]" +mode.audio_volume.icon.inactive = "@ph/microphone-slash-light[alpha=90|color=#fc4646]" mode.audio_volume.circle_indicator.color = "#ffffff" mode.audio_volume.circle_indicator.width = 2 mode.audio_volume.circle_indicator.radius = 40 -mode.audio_volume.button_for_mute = true -mode.audio_volume.button_for_unmute = true +mode.audio_volume.disable_press_to_unmute = true +mode.audio_volume.muted_turn_action = "unmute-at-zero" [knobs.left-center] icon = "@apps/discord" diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 87271c8..9ed1fa8 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -1,20 +1,20 @@ -mod model; - use std::thread::sleep; use std::time::{Duration, Instant}; + use color_eyre::eyre::ContextCompat; use color_eyre::Result; use rgb::RGB8; + use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::events::LoupedeckEvent; +mod model; + fn main() -> Result<()> { let available_devices = LoupedeckDevice::discover()?; - let device = available_devices.first() - .wrap_err("at least one device should be connected")? - .connect()?; + let device = available_devices.first().wrap_err("at least one device should be connected")?.connect()?; println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number()); device.set_brightness(1.0); @@ -30,7 +30,12 @@ fn run_rainbow(device: &LoupedeckDevice) -> Result<()> { let start = Instant::now(); let mut iteration = 0; - let buttons = device.characteristics().available_buttons.iter().filter(|b| b.supports_color()).collect::>(); + let buttons = device + .characteristics() + .available_buttons + .iter() + .filter(|b| b.supports_color()) + .collect::>(); 'outer: loop { let ms = start.elapsed().as_millis() as u64; @@ -50,24 +55,29 @@ fn run_rainbow(device: &LoupedeckDevice) -> Result<()> { for (index, button) in buttons.iter().enumerate() { let t = (ms + (index * 100) as u64) as f32; - device.set_button_color(*button, RGB8::new( - (((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, - (((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, - (((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, - ))?; + device.set_button_color( + *button, + RGB8::new( + (((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, + (((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, + (((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, + ), + )?; } sleep((iteration + 1) * interval - start.elapsed()); iteration += 1; - }; + } Ok(()) } fn run_vibrations(device: &LoupedeckDevice) -> Result<()> { for event in device.events_channel() { - if let LoupedeckEvent::Touch { is_end: false, .. } = event { device.vibrate(VibrationPattern::Low) } + if let LoupedeckEvent::Touch { is_end: false, .. } = event { + device.vibrate(VibrationPattern::Low) + } } Ok(()) -} \ No newline at end of file +} diff --git a/deckster/src/model/deckster_file.rs b/deckster/src/model/deckster_file.rs deleted file mode 100644 index 18090f0..0000000 --- a/deckster/src/model/deckster_file.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct DecksterFile { - key_pages: Vec, - button_pages: Vec, - knob_pages: Vec, - - initial: DecksterFileInitial, - icon_packs: Vec -} - -#[derive(Debug, Deserialize)] -pub struct DecksterFileInitial { - key_page: String, - button_page: String, - knob_page: String -} - -#[derive(Debug, Deserialize)] -pub struct DecksterFileIconPack { - id: String, - path: String, - global_filter: String -} \ No newline at end of file diff --git a/deckster/src/model/files/deckster.rs b/deckster/src/model/files/deckster.rs new file mode 100644 index 0000000..01771bc --- /dev/null +++ b/deckster/src/model/files/deckster.rs @@ -0,0 +1,45 @@ +use rgb::RGB8; +use serde::Deserialize; + +use crate::model::image_filter::ImageFilter; +use crate::model::rgb::DeserializableRGB8; + +#[derive(Debug, Deserialize)] +pub struct File { + pub key_pages: Vec, + pub knob_pages: Vec, + pub icon_packs: Vec, + #[serde(default = "inactive_button_color_default")] + pub inactive_button_color: DeserializableRGB8, + #[serde(default = "active_button_color_default")] + pub active_button_color: DeserializableRGB8, + pub buttons: [Option; 8], + pub initial: InitialConfig, +} + +fn inactive_button_color_default() -> DeserializableRGB8 { + DeserializableRGB8(RGB8::new(128, 128, 128)) +} + +fn active_button_color_default() -> DeserializableRGB8 { + DeserializableRGB8(RGB8::new(0, 255, 0)) +} + +#[derive(Debug, Deserialize)] +pub struct ButtonConfig { + pub key_page: Option, + pub knob_page: Option, +} + +#[derive(Debug, Deserialize)] +pub struct InitialConfig { + pub key_page: String, + pub knob_page: String, +} + +#[derive(Debug, Deserialize)] +pub struct IconPack { + pub id: String, + pub path: String, + pub global_filter: Option, +} diff --git a/deckster/src/model/files/key_page.rs b/deckster/src/model/files/key_page.rs new file mode 100644 index 0000000..636cb59 --- /dev/null +++ b/deckster/src/model/files/key_page.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::model::geometry::UIntVec2; +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::{key_modes, KnobPosition}; + +#[derive(Debug, Deserialize)] +pub struct File { + pub scrolling: ScrollingConfig, + pub keys: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct ScrollingConfig { + pub knob: Option, + pub key: Option, + pub delta: u8, + #[serde(default)] + pub axis: ScrollingConfigAxis, +} + +#[derive(Debug, Deserialize)] +pub struct ScrollingKeysConfig { + pub previous: UIntVec2, + pub next: UIntVec2, +} + +#[derive(Debug, Default, Deserialize)] +pub enum ScrollingConfigAxis { + #[default] + Vertical, + Horizontal, +} + +#[derive(Debug, Deserialize)] +pub struct KeyConfig { + pub label: Option, + pub icon: Option, + #[serde(default)] + pub mode: KeyModes, +} + +#[allow(non_snake_case)] +#[derive(Debug, Deserialize, Default)] +pub struct KeyModes { + pub vibrate: Option, + pub media__play_pause: Option, + pub media__previous: Option, + pub media__next: Option, + pub spotify__shuffle: Option, + pub spotify__repeat: Option, + pub home_assistant__switch: Option, + pub home_assistant__button: Option, +} diff --git a/deckster/src/model/files/knob_page.rs b/deckster/src/model/files/knob_page.rs new file mode 100644 index 0000000..bffa5b2 --- /dev/null +++ b/deckster/src/model/files/knob_page.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::{knob_modes, KnobPosition}; + +#[derive(Debug, Deserialize)] +pub struct File { + pub knobs: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct Knob { + pub icon: IconDescriptor, + pub mode: KnobModes, +} + +#[derive(Debug, Deserialize)] +pub struct KnobModes { + pub audio_volume: knob_modes::audio_volume::Config, +} diff --git a/deckster/src/model/files/mod.rs b/deckster/src/model/files/mod.rs new file mode 100644 index 0000000..76a823b --- /dev/null +++ b/deckster/src/model/files/mod.rs @@ -0,0 +1,3 @@ +pub mod deckster; +pub mod key_page; +pub mod knob_page; diff --git a/deckster/src/model/geometry.rs b/deckster/src/model/geometry.rs index 1f74296..7119cd5 100644 --- a/deckster/src/model/geometry.rs +++ b/deckster/src/model/geometry.rs @@ -1,19 +1,48 @@ +use std::fmt::{Display, Formatter}; use std::str::FromStr; use piet::kurbo::{Rect, Vec2}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use thiserror::Error; -pub fn parse_positive_int_vec2_from_str(s: &str, separator: char) -> Result { - let values = s.split_once(separator); +#[derive(Debug, SerializeDisplay, DeserializeFromStr, Eq, PartialEq, Hash)] +pub struct UIntVec2 { + x: u64, + y: u64, +} - if let Some((x, y)) = values { - if let Some(x) = usize::from_str(x).ok() { - if let Some(y) = usize::from_str(y).ok() { - return Ok(Vec2::new(x as f64, y as f64)); +impl UIntVec2 { + fn to_kurbo_vec2(&self) -> Vec2 { + Vec2::new(self.x as f64, self.y as f64) + } +} + +#[derive(Debug, Error)] +#[error("The input value does not match the required format of and separated by an 'x'")] +pub struct UIntVec2FromStrError {} + +impl FromStr for UIntVec2 { + type Err = UIntVec2FromStrError; + + fn from_str(s: &str) -> Result { + let values = s.split_once('x'); + + if let Some((x, y)) = values { + if let Ok(x) = u64::from_str(x) { + if let Ok(y) = u64::from_str(y) { + return Ok(UIntVec2 { x, y }); + } } } - } - Err(()) + Err(UIntVec2FromStrError {}) + } +} + +impl Display for UIntVec2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}x{}", self.x, self.y)) + } } pub fn parse_positive_rect_from_str(s: &str) -> Result { @@ -25,8 +54,8 @@ pub fn parse_positive_rect_from_str(s: &str) -> Result { return Err(()); }; - let first_vec = parse_positive_int_vec2_from_str(pairs.0, 'x')?; - let second_vec = parse_positive_int_vec2_from_str(pairs.1, 'x')?; + let first_vec = UIntVec2::from_str(pairs.0).map_err(|_| ())?.to_kurbo_vec2(); + let second_vec = UIntVec2::from_str(pairs.1).map_err(|_| ())?.to_kurbo_vec2(); Ok(if is_corner_points_mode { Rect::from_points(first_vec.to_point(), second_vec.to_point()) diff --git a/deckster/src/model/icon_descriptor.rs b/deckster/src/model/icon_descriptor.rs new file mode 100644 index 0000000..8073cae --- /dev/null +++ b/deckster/src/model/icon_descriptor.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use serde_with::DeserializeFromStr; +use thiserror::Error; + +use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; + +#[derive(Debug, DeserializeFromStr)] +pub struct IconDescriptor { + pub source: IconDescriptorSource, + pub filter: Option, +} + +#[derive(Debug, Error)] +pub enum IconDescriptorFromStrError { + #[error("Not a valid icon identifier: {0}")] + InvalidIconPackSource(String), + + #[error("The image filter is invalid: {0}")] + InvalidImageFilter(#[source] ImageFilterFromStringError), + + #[error("The image filter is missing the closing ']'")] + MissingImageFilterClosingBracket, +} + +impl FromStr for IconDescriptor { + type Err = IconDescriptorFromStrError; + + fn from_str(s: &str) -> Result { + let (raw_source, raw_filter) = s.split_once('[').unwrap_or((s, "")); + + let source = if raw_source.starts_with('@') { + let (pack_id, icon_id) = raw_source + .split_once('/') + .ok_or_else(|| IconDescriptorFromStrError::InvalidIconPackSource(raw_source.to_string()))?; + IconDescriptorSource::IconPack { + pack_id: pack_id.to_owned(), + icon_id: icon_id.to_owned(), + } + } else { + IconDescriptorSource::Path(PathBuf::from(raw_source)) + }; + + let filter: Option = if raw_filter.is_empty() { + None + } else { + let mut raw_filter = raw_filter.to_owned(); + if raw_filter.pop().expect("emptiness was eliminated a few lines earlier") != ']' { + return Err(IconDescriptorFromStrError::MissingImageFilterClosingBracket); + } + + Some(ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?) + }; + + Ok(IconDescriptor { source, filter }) + } +} + +#[derive(Debug)] +pub enum IconDescriptorSource { + IconPack { pack_id: String, icon_id: String }, + Path(PathBuf), +} diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 4c14450..7126343 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -1,22 +1,24 @@ -use crate::model::geometry::parse_positive_rect_from_str; -use crate::model::rgb::parse_rgb8_from_hex_str; +use std::str::FromStr; + use piet::kurbo::Rect; use rgb::RGB8; use serde_with::DeserializeFromStr; -use std::str::FromStr; use thiserror::Error; -#[derive(DeserializeFromStr)] +use crate::model::geometry::parse_positive_rect_from_str; +use crate::model::rgb::parse_rgb8_from_hex_str; + +#[derive(Debug, DeserializeFromStr)] pub struct ImageFilter { - crop_original: Option, // applied before scale and rotate - scale: f32, - clockwise_quarter_rotations: u8, - crop: Option, // applied after scale and rotate - color: Option, - alpha: f32, - blur: f32, - grayscale: bool, - invert: bool, + pub crop_original: Option, // applied before scale and rotate + pub scale: f32, + pub clockwise_quarter_rotations: u8, + pub crop: Option, // applied after scale and rotate + pub color: Option, + pub alpha: f32, + pub blur: f32, + pub grayscale: bool, + pub invert: bool, } #[derive(Debug, Error)] diff --git a/deckster/src/model/key_modes/home_assistant.rs b/deckster/src/model/key_modes/home_assistant.rs new file mode 100644 index 0000000..6d71a2b --- /dev/null +++ b/deckster/src/model/key_modes/home_assistant.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; + +#[derive(Debug, Deserialize)] +pub struct SwitchConfig { + pub name: String, + #[serde(default)] + pub icon: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct ButtonConfig { + pub name: String, + #[serde(default)] + pub icon: HashMap, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum SwitchState { + Unavailable, + On, + Off, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ButtonState { + Unavailable, + Available, +} diff --git a/deckster/src/model/key_modes/media.rs b/deckster/src/model/key_modes/media.rs new file mode 100644 index 0000000..fe9ac1b --- /dev/null +++ b/deckster/src/model/key_modes/media.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; + +#[derive(Debug, Deserialize)] +pub struct PlayPauseConfig { + #[serde(default)] + pub icon: HashMap, + #[serde(default)] + pub action: PlayPauseAction, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PlayPauseAction { + #[default] + Toggle, + Play, + Pause, +} + +#[derive(Debug, Deserialize)] +pub struct PreviousAndNextConfig { + #[serde(default)] + pub icon: HashMap, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PlayPauseState { + Inactive, + Playing, + Paused, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PreviousAndNextState { + Inactive, + Active, +} diff --git a/deckster/src/model/key_modes/mod.rs b/deckster/src/model/key_modes/mod.rs new file mode 100644 index 0000000..9febb99 --- /dev/null +++ b/deckster/src/model/key_modes/mod.rs @@ -0,0 +1,5 @@ +pub mod home_assistant; +pub mod media; +pub mod spotify; +pub mod timer; +pub mod vibrate; diff --git a/deckster/src/model/key_modes/spotify.rs b/deckster/src/model/key_modes/spotify.rs new file mode 100644 index 0000000..612a424 --- /dev/null +++ b/deckster/src/model/key_modes/spotify.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; + +#[derive(Debug, Deserialize)] +pub struct ShuffleConfig { + #[serde(default)] + pub icon: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct RepeatConfig { + #[serde(default)] + pub icon: HashMap, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ShuffleState { + Inactive, + Active, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum RepeatState { + Inactive, + Single, + All, +} diff --git a/deckster/src/model/key_modes/timer.rs b/deckster/src/model/key_modes/timer.rs new file mode 100644 index 0000000..e99dcb3 --- /dev/null +++ b/deckster/src/model/key_modes/timer.rs @@ -0,0 +1,15 @@ +use std::time::Duration; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub durations: Vec, + #[serde(default)] + pub vibrate_when_finished: bool, + #[serde(default)] + pub needy: bool, +} + +#[derive(Debug, Deserialize)] +pub struct DurationWrapper(#[serde(with = "humantime_serde")] pub Duration); diff --git a/deckster/src/model/key_modes/vibrate.rs b/deckster/src/model/key_modes/vibrate.rs new file mode 100644 index 0000000..928ba84 --- /dev/null +++ b/deckster/src/model/key_modes/vibrate.rs @@ -0,0 +1,39 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub pattern: VibrationPattern, +} + +#[derive(Debug, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum VibrationPattern { + Short, + Medium, + Long, + Low, + ShortLow, + ShortLower, + Lower, + Lowest, + DescendSlow, + DescendMed, + DescendFast, + AscendSlow, + AscendMed, + AscendFast, + RevSlowest, + RevSlow, + RevMed, + RevFast, + RevFaster, + RevFastest, + RiseFall, + Buzz, + Rumble5, + Rumble4, + Rumble3, + Rumble2, + Rumble1, + VeryLong, +} diff --git a/deckster/src/model/knob_modes/audio_volume.rs b/deckster/src/model/knob_modes/audio_volume.rs new file mode 100644 index 0000000..07f2d1f --- /dev/null +++ b/deckster/src/model/knob_modes/audio_volume.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use std::num::NonZeroU8; + +use regex::Regex; +use serde::Deserialize; + +use crate::model::icon_descriptor::IconDescriptor; +use crate::model::rgb::DeserializableRGB8WithOptionalAlpha; + +#[derive(Debug, Deserialize)] +pub struct Config { + #[serde(with = "serde_regex")] + pub regex: Regex, + #[serde(default)] + pub direction: Direction, + #[serde(default)] + pub disable_press_to_mute: bool, + #[serde(default)] + pub disable_press_to_unmute: bool, + #[serde(default)] + pub muted_turn_action: MutedTurnAction, + #[serde(default)] + pub label: HashMap, + #[serde(default)] + pub icon: HashMap, + pub circle_indicator: Option, + pub bar_indicator: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum MutedTurnAction { + #[default] + Ignore, + UnmuteAtZero, + UnmuteAndRestore, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Direction { + #[default] + Output, + Input, +} + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum State { + Inactive, + Active, + Muted, +} + +#[derive(Debug, Deserialize)] +pub struct CircleIndicatorConfig { + pub color: DeserializableRGB8WithOptionalAlpha, + pub width: NonZeroU8, + pub radius: u8, +} + +#[derive(Debug, Deserialize)] +pub struct BarIndicatorConfig { + pub color: DeserializableRGB8WithOptionalAlpha, +} diff --git a/deckster/src/model/knob_modes/mod.rs b/deckster/src/model/knob_modes/mod.rs new file mode 100644 index 0000000..322ca72 --- /dev/null +++ b/deckster/src/model/knob_modes/mod.rs @@ -0,0 +1 @@ +pub mod audio_volume; diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index 35ee79e..fb1c44e 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -1,4 +1,20 @@ -mod deckster_file; -mod image_filter; +use serde::Deserialize; + +mod files; mod geometry; -mod rgb; \ No newline at end of file +mod icon_descriptor; +mod image_filter; +mod key_modes; +mod knob_modes; +mod rgb; + +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum KnobPosition { + LeftTop, + LeftMiddle, + LeftBottom, + RightTop, + RightMiddle, + RightBottom, +} diff --git a/deckster/src/model/rgb.rs b/deckster/src/model/rgb.rs index f62a464..40c5f14 100644 --- a/deckster/src/model/rgb.rs +++ b/deckster/src/model/rgb.rs @@ -1,37 +1,78 @@ -use rgb::{RGB8, RGBA8}; +use std::str::FromStr; -pub fn parse_rgb8_from_hex_str(s: &str) -> Result { +use rgb::{RGB8, RGBA8}; +use serde_with::DeserializeFromStr; +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("The input value does not match the required format.")] +pub struct RGBParsingError {} + +pub fn parse_rgb8_from_hex_str(s: &str) -> Result { let first_index = if s.starts_with('#') { 1 } else { 0 }; if s.len() - first_index == 6 { - let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ())?; - let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ())?; - let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ())?; + let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| RGBParsingError {})?; + let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| RGBParsingError {})?; + let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| RGBParsingError {})?; return Ok(RGB8::new(r, g, b)); } - Err(()) + Err(RGBParsingError {}) } -pub fn parse_rgba8_from_hex_str(s: &str) -> Result { +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 { - let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ())?; - let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ())?; - let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ())?; - let a = u8::from_str_radix(&s[(first_index + 6)..(first_index + 8)], 16).map_err(|_| ())?; + let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| RGBParsingError {})?; + let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| RGBParsingError {})?; + let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| RGBParsingError {})?; + let a = u8::from_str_radix(&s[(first_index + 6)..(first_index + 8)], 16).map_err(|_| RGBParsingError {})?; return Ok(RGBA8::new(r, g, b, a)); } - Err(()) + Err(RGBParsingError {}) } -pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result { +pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result { // optionally +1 for the '#' match s.len() { 6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)), 8 | 9 => Ok(parse_rgba8_from_hex_str(s)?), - _ => Err(()), + _ => Err(RGBParsingError {}), + } +} + +#[derive(Debug, DeserializeFromStr)] +pub struct DeserializableRGB8(pub RGB8); + +impl FromStr for DeserializableRGB8 { + type Err = RGBParsingError; + + fn from_str(s: &str) -> Result { + parse_rgb8_from_hex_str(s).map(|v| DeserializableRGB8(v)) + } +} + +#[derive(Debug, DeserializeFromStr)] +pub struct DeserializableRGBA8(pub RGBA8); + +impl FromStr for DeserializableRGBA8 { + type Err = RGBParsingError; + + fn from_str(s: &str) -> Result { + parse_rgba8_from_hex_str(s).map(|v| DeserializableRGBA8(v)) + } +} + +#[derive(Debug, DeserializeFromStr)] +pub struct DeserializableRGB8WithOptionalAlpha(pub RGBA8); + +impl FromStr for DeserializableRGB8WithOptionalAlpha { + type Err = RGBParsingError; + + fn from_str(s: &str) -> Result { + parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(|v| DeserializableRGB8WithOptionalAlpha(v)) } } diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index 5912729..a4139ca 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -1,5 +1,6 @@ use enum_ordinalize::Ordinalize; use enumset::{enum_set, EnumSet, EnumSetType}; + use crate::util::Endianness; #[derive(Debug, Ordinalize, EnumSetType)] @@ -16,12 +17,12 @@ pub enum LoupedeckKnob { #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u8)] pub enum LoupedeckButton { - KnobTopLeft = 0x01, - KnobCenterLeft = 0x02, - KnobBottomLeft = 0x03, - KnobTopRight = 0x04, - KnobCenterRight = 0x05, - KnobBottomRight = 0x06, + KnobLeftTop = 0x01, + KnobLeftCenter = 0x02, + KnobLeftBottom = 0x03, + KnobRightTop = 0x04, + KnobRightCenter = 0x05, + KnobRightBottom = 0x06, N0 = 0x07, N1 = 0x08, N2 = 0x09, @@ -69,13 +70,19 @@ pub struct LoupedeckDeviceCharacteristics { impl LoupedeckDeviceCharacteristics { pub fn key_size(&self) -> (u16, u16) { // Assuming the sizes are integers - (self.key_grid_display.width / self.key_grid_columns as u16, self.key_grid_display.height / self.key_grid_rows as u16) + ( + self.key_grid_display.width / self.key_grid_columns as u16, + self.key_grid_display.height / self.key_grid_rows as u16, + ) } pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> { - let check = |display: &&LoupedeckDeviceDisplayConfiguration| - x >= display.global_offset_x && x <= display.global_offset_x + display.width && - y >= display.global_offset_y && y <= display.global_offset_y + display.height; + let check = |display: &&LoupedeckDeviceDisplayConfiguration| { + x >= display.global_offset_x + && x <= display.global_offset_x + display.width + && y >= display.global_offset_y + && y <= display.global_offset_y + display.height + }; if check(&&self.key_grid_display) { Some(&self.key_grid_display) @@ -109,26 +116,30 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck vendor_id: 0x2ec2, product_id: 0x0004, name: "Loupedeck Live", - available_knobs: enum_set!(LoupedeckKnob::KnobTopLeft - | LoupedeckKnob::KnobCenterLeft - | LoupedeckKnob::KnobBottomLeft - | LoupedeckKnob::KnobTopRight - | LoupedeckKnob::KnobCenterRight - | LoupedeckKnob::KnobBottomRight), - available_buttons: enum_set!(LoupedeckButton::KnobTopLeft - | LoupedeckButton::KnobCenterLeft - | LoupedeckButton::KnobBottomLeft - | LoupedeckButton::KnobTopRight - | LoupedeckButton::KnobCenterRight - | LoupedeckButton::KnobBottomRight - | LoupedeckButton::N0 - | LoupedeckButton::N1 - | LoupedeckButton::N2 - | LoupedeckButton::N3 - | LoupedeckButton::N4 - | LoupedeckButton::N5 - | LoupedeckButton::N6 - | LoupedeckButton::N7), + available_knobs: enum_set!( + LoupedeckKnob::KnobTopLeft + | LoupedeckKnob::KnobCenterLeft + | LoupedeckKnob::KnobBottomLeft + | LoupedeckKnob::KnobTopRight + | LoupedeckKnob::KnobCenterRight + | LoupedeckKnob::KnobBottomRight + ), + available_buttons: enum_set!( + LoupedeckButton::KnobLeftTop + | LoupedeckButton::KnobLeftCenter + | LoupedeckButton::KnobLeftBottom + | LoupedeckButton::KnobRightTop + | LoupedeckButton::KnobRightCenter + | LoupedeckButton::KnobRightBottom + | LoupedeckButton::N0 + | LoupedeckButton::N1 + | LoupedeckButton::N2 + | LoupedeckButton::N3 + | LoupedeckButton::N4 + | LoupedeckButton::N5 + | LoupedeckButton::N6 + | LoupedeckButton::N7 + ), key_grid_rows: 3, key_grid_columns: 4, key_grid_display: LoupedeckDeviceDisplayConfiguration { @@ -168,6 +179,4 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck ], }; -pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [ - &LOUPEDECK_LIVE_CHARACTERISTIC -]; \ No newline at end of file +pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [&LOUPEDECK_LIVE_CHARACTERISTIC]; diff --git a/loupedeck_serial/src/commands.rs b/loupedeck_serial/src/commands.rs index 02ffd89..9d3580f 100644 --- a/loupedeck_serial/src/commands.rs +++ b/loupedeck_serial/src/commands.rs @@ -1,6 +1,7 @@ use bytes::Bytes; use enum_ordinalize::Ordinalize; use rgb::RGB8; + use crate::characteristics::LoupedeckButton; #[derive(Debug, Ordinalize)] @@ -57,6 +58,6 @@ pub(crate) enum LoupedeckCommand { display_id: u8, }, Vibrate { - pattern: VibrationPattern + pattern: VibrationPattern, }, -} \ No newline at end of file +} diff --git a/loupedeck_serial/src/events.rs b/loupedeck_serial/src/events.rs index 59aa1e8..563e5e3 100644 --- a/loupedeck_serial/src/events.rs +++ b/loupedeck_serial/src/events.rs @@ -3,36 +3,20 @@ use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; #[derive(Debug)] pub enum RotationDirection { Clockwise, - Counterclockwise + Counterclockwise, } #[derive(Debug)] pub(crate) enum LoupedeckInternalEvent { - GetSerialNumberResponse { - serial_number: String - }, - GetFirmwareVersionResponse { - firmware_version: String - } + GetSerialNumberResponse { serial_number: String }, + GetFirmwareVersionResponse { firmware_version: String }, } #[derive(Debug)] pub enum LoupedeckEvent { Disconnected, - ButtonDown { - button: LoupedeckButton - }, - ButtonUp { - button: LoupedeckButton - }, - KnobRotate { - knob: LoupedeckKnob, - direction: RotationDirection - }, - Touch { - touch_id: u8, - x: u16, - y: u16, - is_end: bool - } -} \ No newline at end of file + ButtonDown { button: LoupedeckButton }, + ButtonUp { button: LoupedeckButton }, + KnobRotate { knob: LoupedeckKnob, direction: RotationDirection }, + Touch { touch_id: u8, x: u16, y: u16, is_end: bool }, +} diff --git a/loupedeck_serial/src/lib.rs b/loupedeck_serial/src/lib.rs index 38c4027..bc37191 100644 --- a/loupedeck_serial/src/lib.rs +++ b/loupedeck_serial/src/lib.rs @@ -1,6 +1,6 @@ pub mod characteristics; +pub mod commands; pub mod device; pub mod events; -pub mod commands; mod messages; -mod util; \ No newline at end of file +mod util; diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs index e37082a..eb27aa1 100644 --- a/loupedeck_serial/src/messages.rs +++ b/loupedeck_serial/src/messages.rs @@ -2,13 +2,15 @@ use std::cmp::min; use std::io; use std::io::{ErrorKind, Read, Write}; use std::sync::mpsc; + use bytes::{Buf, BufMut, Bytes, BytesMut}; use enum_ordinalize::Ordinalize; use serialport::SerialPort; + use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; use crate::commands::LoupedeckCommand; -use crate::events::{LoupedeckEvent, LoupedeckInternalEvent}; use crate::events::RotationDirection::{Clockwise, Counterclockwise}; +use crate::events::{LoupedeckEvent, LoupedeckInternalEvent}; pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html HTTP/1.1 @@ -29,7 +31,7 @@ const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize; enum ParseMessageResult { InternalEvent(LoupedeckInternalEvent), PublicEvent(LoupedeckEvent), - Nothing + Nothing, } impl From for ParseMessageResult { @@ -118,35 +120,36 @@ 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"); + 0x00 => { + // Button + let button = LoupedeckButton::from_ordinal(message[0]).expect("Invalid button ID"); match message[1] { 0x00 => LoupedeckEvent::ButtonDown { button }, _ => LoupedeckEvent::ButtonUp { button }, - }.into() + } + .into() } - 0x01 => { // Knob - let knob = LoupedeckKnob::from_ordinal(message[0]) - .expect("Invalid button ID"); + 0x01 => { + // Knob + let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID"); LoupedeckEvent::KnobRotate { knob, direction: if message[1] == 1 { Clockwise } else { Counterclockwise }, - }.into() + } + .into() } - 0x03 => { - LoupedeckInternalEvent::GetSerialNumberResponse { - serial_number: String::from_utf8_lossy(&message).into_owned() - }.into() + 0x03 => LoupedeckInternalEvent::GetSerialNumberResponse { + serial_number: String::from_utf8_lossy(&message).into_owned(), } - 0x07 => { - LoupedeckInternalEvent::GetFirmwareVersionResponse { - firmware_version: format!("{}.{}.{}", message[0], message[1], message[2]) - }.into() + .into(), + 0x07 => LoupedeckInternalEvent::GetFirmwareVersionResponse { + firmware_version: format!("{}.{}.{}", message[0], message[1], message[2]), } - 0x4d | 0x6d => { // Touch + .into(), + 0x4d | 0x6d => { + // Touch message.advance(1); let x = message.get_u16(); let y = message.get_u16(); @@ -157,11 +160,10 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { x, y, is_end: command == 0x6d, - }.into() - } - _ => { - ParseMessageResult::Nothing + } + .into() } + _ => ParseMessageResult::Nothing, } } @@ -209,26 +211,20 @@ pub(crate) fn write_messages_worker(mut port: Box, receiver: cro for command in receiver { let result = match command { - LoupedeckCommand::RequestSerialNumber => { - send(0x03, Bytes::new()) - } - LoupedeckCommand::RequestFirmwareVersion => { - send(0x07, Bytes::new()) - } + LoupedeckCommand::RequestSerialNumber => send(0x03, Bytes::new()), + LoupedeckCommand::RequestFirmwareVersion => send(0x07, Bytes::new()), LoupedeckCommand::SetBrightness(value) => { let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8; send(0x09, Bytes::copy_from_slice(&[raw_value])) } - LoupedeckCommand::SetButtonColor { button, color } => { - send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b])) - } + LoupedeckCommand::SetButtonColor { button, color } => send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b])), LoupedeckCommand::ReplaceFramebufferArea { display_id, x, y, width, height, - buffer + buffer, } => { let mut data = BytesMut::with_capacity(10 + buffer.len()); data.put_u8(0); @@ -241,23 +237,17 @@ pub(crate) fn write_messages_worker(mut port: Box, receiver: cro send(0x10, data.freeze()) } - LoupedeckCommand::RefreshDisplay { display_id } => { - send(0x0f, Bytes::copy_from_slice(&[0, display_id])) - } - LoupedeckCommand::Vibrate { pattern } => { - send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])) - } + LoupedeckCommand::RefreshDisplay { display_id } => send(0x0f, Bytes::copy_from_slice(&[0, display_id])), + LoupedeckCommand::Vibrate { pattern } => send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])), }; if let Err(error) = result { match error.kind() { - ErrorKind::TimedOut | ErrorKind::BrokenPipe => { - break - } + ErrorKind::TimedOut | ErrorKind::BrokenPipe => break, _ => { panic!("IO error during write: {}", error); } } } } -} \ No newline at end of file +} diff --git a/loupedeck_serial/src/util.rs b/loupedeck_serial/src/util.rs index 169f79b..3f3364f 100644 --- a/loupedeck_serial/src/util.rs +++ b/loupedeck_serial/src/util.rs @@ -28,5 +28,5 @@ pub(crate) fn convert_rgb888_to_rgb565(original: Bytes, endianness: Endianness) #[derive(Debug, Copy, Clone)] pub enum Endianness { BigEndian, - LittleEndian -} \ No newline at end of file + LittleEndian, +}