Add model for whole example

This commit is contained in:
Moritz Ruth 2023-12-31 00:59:27 +01:00
parent 3fe1912389
commit 634a1aa547
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
34 changed files with 756 additions and 263 deletions

2
Cargo.lock generated
View file

@ -227,6 +227,7 @@ dependencies = [
"humantime-serde", "humantime-serde",
"loupedeck_serial", "loupedeck_serial",
"piet", "piet",
"regex",
"rgb", "rgb",
"serde", "serde",
"serde_regex", "serde_regex",
@ -643,7 +644,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"serde",
] ]
[[package]] [[package]]

View file

@ -8,9 +8,10 @@ color-eyre = "0.6.2"
humantime-serde = "1.1.1" humantime-serde = "1.1.1"
loupedeck_serial = { path = "../loupedeck_serial" } loupedeck_serial = { path = "../loupedeck_serial" }
piet = "0.6.2" piet = "0.6.2"
rgb = { version = "0.8.37", features = ["serde"] } rgb = "0.8.37"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
serde_regex = "1.1.0" serde_regex = "1.1.0"
serde_with = "3.4.0" serde_with = "3.4.0"
thiserror = "1.0.52" thiserror = "1.0.52"
toml = "0.8.8" toml = "0.8.8"
regex = "1.10.2"

View file

@ -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" }

View file

@ -1,10 +1,23 @@
key_pages = ["./key-pages/*"] key_pages = ["./key-pages/*"]
button_pages = ["./key-pages/*"] knob_pages = ["./knob-pages/*"]
knob_pages = ["./button-page.toml"]
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] [initial]
key_page = "default" key_page = "default"
button_page = "default"
knob_page = "default" knob_page = "default"
[[icon_packs]] [[icon_packs]]

View file

@ -1,28 +1,31 @@
[defaults] [keys.1x1]
mode.vibrate = "low" 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] [keys.1x2]
mode.media__play_pause.icon.play = "@ph/play"
mode.media__play_pause.icon.pause = "@ph/pause"
[keys.1-2]
icon = "@fad/shuffle" 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" icon = "@ph/timer"
mode.vibrate.pattern = "low"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"] mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_on_finish = true mode.timer.vibrate_when_finished = true
mode.timer.needy = true mode.timer.needy = true
[keys.3-3] [keys.3x3]
icon = "@fad/thunderbolt" icon = "@fad/thunderbolt"
label = "Dock" label = "Dock"
mode.homeassistant__switch.name = "switch.moritz_thunderbolt_dock" mode.vibrate.pattern = "low"
mode.homeassistant__switch.border_color.is_on = "#58fc11" 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" icon = "@ph/computer-tower"
label = "Tower PC" label = "Tower PC"
mode.homeassistant__switch.name = "switch.mwin" mode.vibrate.pattern = "low"
mode.homeassistant__switch.border_color.is_on = "#58fc11" mode.home_assistant__switch.name = "switch.mwin"
mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]"

View file

@ -1,22 +1,28 @@
[scrolling] [scrolling]
knob = "bottom-right" knob = "right-bottom"
axis = "y" axis = "vertical"
delta = 1 delta = 1
[keys.4-1] [keys.4x1]
label = "😀"
mode.keyboard.key = "😀" mode.keyboard.key = "😀"
[keys.4-2] [keys.4x2]
label = "😄"
mode.keyboard.key = "😄" mode.keyboard.key = "😄"
[keys.4-3] [keys.4x3]
label = "😅"
mode.keyboard.key = "😅" mode.keyboard.key = "😅"
[keys.4-4] [keys.4x4]
label = "😂"
mode.keyboard.key = "😂" mode.keyboard.key = "😂"
[keys.4-5] [keys.4x5]
label = "😁"
mode.keyboard.key = "😁" mode.keyboard.key = "😁"
[keys.4-6] [keys.4x6]
label = "😇"
mode.keyboard.key = "😇" mode.keyboard.key = "😇"

View file

@ -1,35 +1,35 @@
[keys.1-4] [keys.1x4]
label = "9" label = "9"
mode.keyboard = { key = "9" } mode.keyboard.key = "9"
[keys.1-3] [keys.1x3]
label = "8" label = "8"
mode.keyboard = { key = "8" } mode.keyboard.key = "8"
[keys.1-2] [keys.1x2]
label = "7" label = "7"
mode.keyboard = { key = "7" } mode.keyboard.key = "7"
[keys.2-4] [keys.2x4]
label = "6" label = "6"
mode.keyboard = { key = "6" } mode.keyboard.key = "6"
[keys.2-3] [keys.2x3]
label = "5" label = "5"
mode.keyboard = { key = "5" } mode.keyboard.key = "5"
[keys.2-2] [keys.2x2]
label = "4" label = "4"
mode.keyboard = { key = "4" } mode.keyboard.key = "4"
[keys.1-4] [keys.1x4]
label = "3" label = "3"
mode.keyboard = { key = "3" } mode.keyboard.key = "3"
[keys.1-3] [keys.1x3]
label = "2" label = "2"
mode.keyboard = { key = "2" } mode.keyboard.key = "2"
[keys.1-2] [keys.1x2]
label = "1" label = "1"
mode.keyboard = { key = "1" } mode.keyboard.key = "1"

View file

@ -1,23 +1,29 @@
[scrolling] [scrolling]
button.previous = "3-3" button.previous = "3x3"
button.next = "4-3" button.next = "4x3"
axis = "x" axis = "horizontal"
delta = 4 delta = 4
[keys.1-1] [keys.1x1]
label = ""
mode.keyboard.key = "" mode.keyboard.key = ""
[keys.2-1] [keys.2x1]
label = "…"
mode.keyboard.key = "…" mode.keyboard.key = "…"
[keys.3-1] [keys.3x1]
label = "—"
mode.keyboard.key = "—" mode.keyboard.key = "—"
[keys.4-1] [keys.4x1]
label = ""
mode.keyboard.key = "" mode.keyboard.key = ""
[keys.5-1] [keys.5x1]
label = "·"
mode.keyboard.key = "·" mode.keyboard.key = "·"
[keys.6-1] [keys.6x1]
label = "→"
mode.keyboard.key = "→" mode.keyboard.key = "→"

View file

@ -1,14 +1,14 @@
[knobs.left-top] [knobs.left-top]
icon = "@ph/microphone-light" icon = "@ph/microphone-light"
mode.audio_volume.mode = "input" mode.audio_volume.direction = "input"
mode.audio_volume.regex = "Microphone" mode.audio_volume.regex = "Microphone"
mode.audio_volume.label.muted = "Muted" 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.color = "#ffffff"
mode.audio_volume.circle_indicator.width = 2 mode.audio_volume.circle_indicator.width = 2
mode.audio_volume.circle_indicator.radius = 40 mode.audio_volume.circle_indicator.radius = 40
mode.audio_volume.button_for_mute = true mode.audio_volume.disable_press_to_unmute = true
mode.audio_volume.button_for_unmute = true mode.audio_volume.muted_turn_action = "unmute-at-zero"
[knobs.left-center] [knobs.left-center]
icon = "@apps/discord" icon = "@apps/discord"

View file

@ -1,20 +1,20 @@
mod model;
use std::thread::sleep; use std::thread::sleep;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use color_eyre::eyre::ContextCompat; use color_eyre::eyre::ContextCompat;
use color_eyre::Result; use color_eyre::Result;
use rgb::RGB8; use rgb::RGB8;
use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::LoupedeckEvent; use loupedeck_serial::events::LoupedeckEvent;
mod model;
fn main() -> Result<()> { fn main() -> Result<()> {
let available_devices = LoupedeckDevice::discover()?; let available_devices = LoupedeckDevice::discover()?;
let device = available_devices.first() let device = available_devices.first().wrap_err("at least one device should be connected")?.connect()?;
.wrap_err("at least one device should be connected")?
.connect()?;
println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number()); println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number());
device.set_brightness(1.0); device.set_brightness(1.0);
@ -30,7 +30,12 @@ fn run_rainbow(device: &LoupedeckDevice) -> Result<()> {
let start = Instant::now(); let start = Instant::now();
let mut iteration = 0; let mut iteration = 0;
let buttons = device.characteristics().available_buttons.iter().filter(|b| b.supports_color()).collect::<Vec<_>>(); let buttons = device
.characteristics()
.available_buttons
.iter()
.filter(|b| b.supports_color())
.collect::<Vec<_>>();
'outer: loop { 'outer: loop {
let ms = start.elapsed().as_millis() as u64; let ms = start.elapsed().as_millis() as u64;
@ -50,23 +55,28 @@ fn run_rainbow(device: &LoupedeckDevice) -> Result<()> {
for (index, button) in buttons.iter().enumerate() { for (index, button) in buttons.iter().enumerate() {
let t = (ms + (index * 100) as u64) as f32; let t = (ms + (index * 100) as u64) as f32;
device.set_button_color(*button, RGB8::new( device.set_button_color(
*button,
RGB8::new(
(((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((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 / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8,
(((t / 250.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()); sleep((iteration + 1) * interval - start.elapsed());
iteration += 1; iteration += 1;
}; }
Ok(()) Ok(())
} }
fn run_vibrations(device: &LoupedeckDevice) -> Result<()> { fn run_vibrations(device: &LoupedeckDevice) -> Result<()> {
for event in device.events_channel() { 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(()) Ok(())

View file

@ -1,25 +0,0 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct DecksterFile {
key_pages: Vec<String>,
button_pages: Vec<String>,
knob_pages: Vec<String>,
initial: DecksterFileInitial,
icon_packs: Vec<DecksterFileIconPack>
}
#[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
}

View file

@ -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<String>,
pub knob_pages: Vec<String>,
pub icon_packs: Vec<IconPack>,
#[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<ButtonConfig>; 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<String>,
pub knob_page: Option<String>,
}
#[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<ImageFilter>,
}

View file

@ -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<UIntVec2, KeyConfig>,
}
#[derive(Debug, Deserialize)]
pub struct ScrollingConfig {
pub knob: Option<KnobPosition>,
pub key: Option<ScrollingKeysConfig>,
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<String>,
pub icon: Option<IconDescriptor>,
#[serde(default)]
pub mode: KeyModes,
}
#[allow(non_snake_case)]
#[derive(Debug, Deserialize, Default)]
pub struct KeyModes {
pub vibrate: Option<key_modes::vibrate::Config>,
pub media__play_pause: Option<key_modes::media::PlayPauseConfig>,
pub media__previous: Option<key_modes::media::PreviousAndNextConfig>,
pub media__next: Option<key_modes::media::PreviousAndNextConfig>,
pub spotify__shuffle: Option<key_modes::spotify::ShuffleConfig>,
pub spotify__repeat: Option<key_modes::spotify::RepeatConfig>,
pub home_assistant__switch: Option<key_modes::home_assistant::SwitchConfig>,
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>,
}

View file

@ -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<KnobPosition, Knob>,
}
#[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,
}

View file

@ -0,0 +1,3 @@
pub mod deckster;
pub mod key_page;
pub mod knob_page;

View file

@ -1,19 +1,48 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
use piet::kurbo::{Rect, Vec2}; 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<Vec2, ()> { #[derive(Debug, SerializeDisplay, DeserializeFromStr, Eq, PartialEq, Hash)]
let values = s.split_once(separator); pub struct UIntVec2 {
x: u64,
y: u64,
}
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 <x> and <y> separated by an 'x'")]
pub struct UIntVec2FromStrError {}
impl FromStr for UIntVec2 {
type Err = UIntVec2FromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values = s.split_once('x');
if let Some((x, y)) = values { if let Some((x, y)) = values {
if let Some(x) = usize::from_str(x).ok() { if let Ok(x) = u64::from_str(x) {
if let Some(y) = usize::from_str(y).ok() { if let Ok(y) = u64::from_str(y) {
return Ok(Vec2::new(x as f64, y as f64)); 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<Rect, ()> { pub fn parse_positive_rect_from_str(s: &str) -> Result<Rect, ()> {
@ -25,8 +54,8 @@ pub fn parse_positive_rect_from_str(s: &str) -> Result<Rect, ()> {
return Err(()); return Err(());
}; };
let first_vec = parse_positive_int_vec2_from_str(pairs.0, 'x')?; let first_vec = UIntVec2::from_str(pairs.0).map_err(|_| ())?.to_kurbo_vec2();
let second_vec = parse_positive_int_vec2_from_str(pairs.1, 'x')?; let second_vec = UIntVec2::from_str(pairs.1).map_err(|_| ())?.to_kurbo_vec2();
Ok(if is_corner_points_mode { Ok(if is_corner_points_mode {
Rect::from_points(first_vec.to_point(), second_vec.to_point()) Rect::from_points(first_vec.to_point(), second_vec.to_point())

View file

@ -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<ImageFilter>,
}
#[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<Self, Self::Err> {
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<ImageFilter> = 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),
}

View file

@ -1,22 +1,24 @@
use crate::model::geometry::parse_positive_rect_from_str; use std::str::FromStr;
use crate::model::rgb::parse_rgb8_from_hex_str;
use piet::kurbo::Rect; use piet::kurbo::Rect;
use rgb::RGB8; use rgb::RGB8;
use serde_with::DeserializeFromStr; use serde_with::DeserializeFromStr;
use std::str::FromStr;
use thiserror::Error; 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 { pub struct ImageFilter {
crop_original: Option<Rect>, // applied before scale and rotate pub crop_original: Option<Rect>, // applied before scale and rotate
scale: f32, pub scale: f32,
clockwise_quarter_rotations: u8, pub clockwise_quarter_rotations: u8,
crop: Option<Rect>, // applied after scale and rotate pub crop: Option<Rect>, // applied after scale and rotate
color: Option<RGB8>, pub color: Option<RGB8>,
alpha: f32, pub alpha: f32,
blur: f32, pub blur: f32,
grayscale: bool, pub grayscale: bool,
invert: bool, pub invert: bool,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -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<SwitchState, IconDescriptor>,
}
#[derive(Debug, Deserialize)]
pub struct ButtonConfig {
pub name: String,
#[serde(default)]
pub icon: HashMap<ButtonState, IconDescriptor>,
}
#[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,
}

View file

@ -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<PlayPauseState, IconDescriptor>,
#[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<PreviousAndNextState, IconDescriptor>,
}
#[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,
}

View file

@ -0,0 +1,5 @@
pub mod home_assistant;
pub mod media;
pub mod spotify;
pub mod timer;
pub mod vibrate;

View file

@ -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<ShuffleState, IconDescriptor>,
}
#[derive(Debug, Deserialize)]
pub struct RepeatConfig {
#[serde(default)]
pub icon: HashMap<RepeatState, IconDescriptor>,
}
#[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,
}

View file

@ -0,0 +1,15 @@
use std::time::Duration;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
pub durations: Vec<DurationWrapper>,
#[serde(default)]
pub vibrate_when_finished: bool,
#[serde(default)]
pub needy: bool,
}
#[derive(Debug, Deserialize)]
pub struct DurationWrapper(#[serde(with = "humantime_serde")] pub Duration);

View file

@ -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,
}

View file

@ -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<State, String>,
#[serde(default)]
pub icon: HashMap<State, IconDescriptor>,
pub circle_indicator: Option<CircleIndicatorConfig>,
pub bar_indicator: Option<BarIndicatorConfig>,
}
#[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,
}

View file

@ -0,0 +1 @@
pub mod audio_volume;

View file

@ -1,4 +1,20 @@
mod deckster_file; use serde::Deserialize;
mod image_filter;
mod files;
mod geometry; mod geometry;
mod icon_descriptor;
mod image_filter;
mod key_modes;
mod knob_modes;
mod rgb; mod rgb;
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
}

View file

@ -1,37 +1,78 @@
use rgb::{RGB8, RGBA8}; use std::str::FromStr;
pub fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, ()> { 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<RGB8, RGBParsingError> {
let first_index = if s.starts_with('#') { 1 } else { 0 }; let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 6 { if s.len() - first_index == 6 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 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(|_| ())?; 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(|_| ())?; let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| RGBParsingError {})?;
return Ok(RGB8::new(r, g, b)); return Ok(RGB8::new(r, g, b));
} }
Err(()) Err(RGBParsingError {})
} }
pub fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ()> { pub fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, RGBParsingError> {
let first_index = if s.starts_with('#') { 1 } else { 0 }; let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 8 { if s.len() - first_index == 8 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 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(|_| ())?; 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(|_| ())?; 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(|_| ())?; 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)); 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<RGBA8, ()> { pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result<RGBA8, RGBParsingError> {
// optionally +1 for the '#' // optionally +1 for the '#'
match s.len() { match s.len() {
6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)), 6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)),
8 | 9 => Ok(parse_rgba8_from_hex_str(s)?), 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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(|v| DeserializableRGB8WithOptionalAlpha(v))
} }
} }

View file

@ -1,5 +1,6 @@
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use enumset::{enum_set, EnumSet, EnumSetType}; use enumset::{enum_set, EnumSet, EnumSetType};
use crate::util::Endianness; use crate::util::Endianness;
#[derive(Debug, Ordinalize, EnumSetType)] #[derive(Debug, Ordinalize, EnumSetType)]
@ -16,12 +17,12 @@ pub enum LoupedeckKnob {
#[derive(Debug, Ordinalize, EnumSetType)] #[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u8)] #[repr(u8)]
pub enum LoupedeckButton { pub enum LoupedeckButton {
KnobTopLeft = 0x01, KnobLeftTop = 0x01,
KnobCenterLeft = 0x02, KnobLeftCenter = 0x02,
KnobBottomLeft = 0x03, KnobLeftBottom = 0x03,
KnobTopRight = 0x04, KnobRightTop = 0x04,
KnobCenterRight = 0x05, KnobRightCenter = 0x05,
KnobBottomRight = 0x06, KnobRightBottom = 0x06,
N0 = 0x07, N0 = 0x07,
N1 = 0x08, N1 = 0x08,
N2 = 0x09, N2 = 0x09,
@ -69,13 +70,19 @@ pub struct LoupedeckDeviceCharacteristics {
impl LoupedeckDeviceCharacteristics { impl LoupedeckDeviceCharacteristics {
pub fn key_size(&self) -> (u16, u16) { pub fn key_size(&self) -> (u16, u16) {
// Assuming the sizes are integers // 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> { pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> {
let check = |display: &&LoupedeckDeviceDisplayConfiguration| let check = |display: &&LoupedeckDeviceDisplayConfiguration| {
x >= display.global_offset_x && x <= display.global_offset_x + display.width && x >= display.global_offset_x
y >= display.global_offset_y && y <= display.global_offset_y + display.height; && 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) { if check(&&self.key_grid_display) {
Some(&self.key_grid_display) Some(&self.key_grid_display)
@ -109,18 +116,21 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
vendor_id: 0x2ec2, vendor_id: 0x2ec2,
product_id: 0x0004, product_id: 0x0004,
name: "Loupedeck Live", name: "Loupedeck Live",
available_knobs: enum_set!(LoupedeckKnob::KnobTopLeft available_knobs: enum_set!(
LoupedeckKnob::KnobTopLeft
| LoupedeckKnob::KnobCenterLeft | LoupedeckKnob::KnobCenterLeft
| LoupedeckKnob::KnobBottomLeft | LoupedeckKnob::KnobBottomLeft
| LoupedeckKnob::KnobTopRight | LoupedeckKnob::KnobTopRight
| LoupedeckKnob::KnobCenterRight | LoupedeckKnob::KnobCenterRight
| LoupedeckKnob::KnobBottomRight), | LoupedeckKnob::KnobBottomRight
available_buttons: enum_set!(LoupedeckButton::KnobTopLeft ),
| LoupedeckButton::KnobCenterLeft available_buttons: enum_set!(
| LoupedeckButton::KnobBottomLeft LoupedeckButton::KnobLeftTop
| LoupedeckButton::KnobTopRight | LoupedeckButton::KnobLeftCenter
| LoupedeckButton::KnobCenterRight | LoupedeckButton::KnobLeftBottom
| LoupedeckButton::KnobBottomRight | LoupedeckButton::KnobRightTop
| LoupedeckButton::KnobRightCenter
| LoupedeckButton::KnobRightBottom
| LoupedeckButton::N0 | LoupedeckButton::N0
| LoupedeckButton::N1 | LoupedeckButton::N1
| LoupedeckButton::N2 | LoupedeckButton::N2
@ -128,7 +138,8 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
| LoupedeckButton::N4 | LoupedeckButton::N4
| LoupedeckButton::N5 | LoupedeckButton::N5
| LoupedeckButton::N6 | LoupedeckButton::N6
| LoupedeckButton::N7), | LoupedeckButton::N7
),
key_grid_rows: 3, key_grid_rows: 3,
key_grid_columns: 4, key_grid_columns: 4,
key_grid_display: LoupedeckDeviceDisplayConfiguration { key_grid_display: LoupedeckDeviceDisplayConfiguration {
@ -168,6 +179,4 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
], ],
}; };
pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [ pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [&LOUPEDECK_LIVE_CHARACTERISTIC];
&LOUPEDECK_LIVE_CHARACTERISTIC
];

View file

@ -1,6 +1,7 @@
use bytes::Bytes; use bytes::Bytes;
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use rgb::RGB8; use rgb::RGB8;
use crate::characteristics::LoupedeckButton; use crate::characteristics::LoupedeckButton;
#[derive(Debug, Ordinalize)] #[derive(Debug, Ordinalize)]
@ -57,6 +58,6 @@ pub(crate) enum LoupedeckCommand {
display_id: u8, display_id: u8,
}, },
Vibrate { Vibrate {
pattern: VibrationPattern pattern: VibrationPattern,
}, },
} }

View file

@ -3,36 +3,20 @@ use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
#[derive(Debug)] #[derive(Debug)]
pub enum RotationDirection { pub enum RotationDirection {
Clockwise, Clockwise,
Counterclockwise Counterclockwise,
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum LoupedeckInternalEvent { pub(crate) enum LoupedeckInternalEvent {
GetSerialNumberResponse { GetSerialNumberResponse { serial_number: String },
serial_number: String GetFirmwareVersionResponse { firmware_version: String },
},
GetFirmwareVersionResponse {
firmware_version: String
}
} }
#[derive(Debug)] #[derive(Debug)]
pub enum LoupedeckEvent { pub enum LoupedeckEvent {
Disconnected, Disconnected,
ButtonDown { ButtonDown { button: LoupedeckButton },
button: LoupedeckButton ButtonUp { button: LoupedeckButton },
}, KnobRotate { knob: LoupedeckKnob, direction: RotationDirection },
ButtonUp { Touch { touch_id: u8, x: u16, y: u16, is_end: bool },
button: LoupedeckButton
},
KnobRotate {
knob: LoupedeckKnob,
direction: RotationDirection
},
Touch {
touch_id: u8,
x: u16,
y: u16,
is_end: bool
}
} }

View file

@ -1,6 +1,6 @@
pub mod characteristics; pub mod characteristics;
pub mod commands;
pub mod device; pub mod device;
pub mod events; pub mod events;
pub mod commands;
mod messages; mod messages;
mod util; mod util;

View file

@ -2,13 +2,15 @@ use std::cmp::min;
use std::io; use std::io;
use std::io::{ErrorKind, Read, Write}; use std::io::{ErrorKind, Read, Write};
use std::sync::mpsc; use std::sync::mpsc;
use bytes::{Buf, BufMut, Bytes, BytesMut}; use bytes::{Buf, BufMut, Bytes, BytesMut};
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use serialport::SerialPort; use serialport::SerialPort;
use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
use crate::commands::LoupedeckCommand; use crate::commands::LoupedeckCommand;
use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
use crate::events::RotationDirection::{Clockwise, Counterclockwise}; use crate::events::RotationDirection::{Clockwise, Counterclockwise};
use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html
HTTP/1.1 HTTP/1.1
@ -29,7 +31,7 @@ const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize;
enum ParseMessageResult { enum ParseMessageResult {
InternalEvent(LoupedeckInternalEvent), InternalEvent(LoupedeckInternalEvent),
PublicEvent(LoupedeckEvent), PublicEvent(LoupedeckEvent),
Nothing Nothing,
} }
impl From<LoupedeckInternalEvent> for ParseMessageResult { impl From<LoupedeckInternalEvent> for ParseMessageResult {
@ -118,35 +120,36 @@ pub(crate) fn read_messages_worker(
fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
match command { match command {
0x00 => { // Button 0x00 => {
let button = LoupedeckButton::from_ordinal(message[0]) // Button
.expect("Invalid button ID"); let button = LoupedeckButton::from_ordinal(message[0]).expect("Invalid button ID");
match message[1] { match message[1] {
0x00 => LoupedeckEvent::ButtonDown { button }, 0x00 => LoupedeckEvent::ButtonDown { button },
_ => LoupedeckEvent::ButtonUp { button }, _ => LoupedeckEvent::ButtonUp { button },
}.into()
} }
0x01 => { // Knob .into()
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 { LoupedeckEvent::KnobRotate {
knob, knob,
direction: if message[1] == 1 { Clockwise } else { Counterclockwise }, direction: if message[1] == 1 { Clockwise } else { Counterclockwise },
}.into()
} }
0x03 => { .into()
LoupedeckInternalEvent::GetSerialNumberResponse {
serial_number: String::from_utf8_lossy(&message).into_owned()
}.into()
} }
0x07 => { 0x03 => LoupedeckInternalEvent::GetSerialNumberResponse {
LoupedeckInternalEvent::GetFirmwareVersionResponse { serial_number: String::from_utf8_lossy(&message).into_owned(),
firmware_version: format!("{}.{}.{}", message[0], message[1], message[2])
}.into()
} }
0x4d | 0x6d => { // Touch .into(),
0x07 => LoupedeckInternalEvent::GetFirmwareVersionResponse {
firmware_version: format!("{}.{}.{}", message[0], message[1], message[2]),
}
.into(),
0x4d | 0x6d => {
// Touch
message.advance(1); message.advance(1);
let x = message.get_u16(); let x = message.get_u16();
let y = message.get_u16(); let y = message.get_u16();
@ -157,11 +160,10 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
x, x,
y, y,
is_end: command == 0x6d, is_end: command == 0x6d,
}.into()
} }
_ => { .into()
ParseMessageResult::Nothing
} }
_ => ParseMessageResult::Nothing,
} }
} }
@ -209,26 +211,20 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: cro
for command in receiver { for command in receiver {
let result = match command { let result = match command {
LoupedeckCommand::RequestSerialNumber => { LoupedeckCommand::RequestSerialNumber => send(0x03, Bytes::new()),
send(0x03, Bytes::new()) LoupedeckCommand::RequestFirmwareVersion => send(0x07, Bytes::new()),
}
LoupedeckCommand::RequestFirmwareVersion => {
send(0x07, Bytes::new())
}
LoupedeckCommand::SetBrightness(value) => { LoupedeckCommand::SetBrightness(value) => {
let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8; let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8;
send(0x09, Bytes::copy_from_slice(&[raw_value])) send(0x09, Bytes::copy_from_slice(&[raw_value]))
} }
LoupedeckCommand::SetButtonColor { button, color } => { LoupedeckCommand::SetButtonColor { button, color } => send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b])),
send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b]))
}
LoupedeckCommand::ReplaceFramebufferArea { LoupedeckCommand::ReplaceFramebufferArea {
display_id, display_id,
x, x,
y, y,
width, width,
height, height,
buffer buffer,
} => { } => {
let mut data = BytesMut::with_capacity(10 + buffer.len()); let mut data = BytesMut::with_capacity(10 + buffer.len());
data.put_u8(0); data.put_u8(0);
@ -241,19 +237,13 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: cro
send(0x10, data.freeze()) send(0x10, data.freeze())
} }
LoupedeckCommand::RefreshDisplay { display_id } => { LoupedeckCommand::RefreshDisplay { display_id } => send(0x0f, Bytes::copy_from_slice(&[0, display_id])),
send(0x0f, Bytes::copy_from_slice(&[0, display_id])) LoupedeckCommand::Vibrate { pattern } => send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])),
}
LoupedeckCommand::Vibrate { pattern } => {
send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()]))
}
}; };
if let Err(error) = result { if let Err(error) = result {
match error.kind() { match error.kind() {
ErrorKind::TimedOut | ErrorKind::BrokenPipe => { ErrorKind::TimedOut | ErrorKind::BrokenPipe => break,
break
}
_ => { _ => {
panic!("IO error during write: {}", error); panic!("IO error during write: {}", error);
} }

View file

@ -28,5 +28,5 @@ pub(crate) fn convert_rgb888_to_rgb565(original: Bytes, endianness: Endianness)
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum Endianness { pub enum Endianness {
BigEndian, BigEndian,
LittleEndian LittleEndian,
} }