commit
This commit is contained in:
parent
82077a6e1e
commit
474ecf4f5e
25 changed files with 864 additions and 256 deletions
167
Cargo.lock
generated
167
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
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"
|
|
@ -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"
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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<IconPack>,
|
||||
#[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<ButtonPosition, ButtonConfig>, // EnumMap
|
||||
pub initial: InitialConfig,
|
||||
}
|
||||
|
@ -31,18 +32,18 @@ pub struct Config {
|
|||
pub key_pages_by_id: HashMap<String, model::key_page::Page>,
|
||||
pub knob_pages_by_id: HashMap<String, model::knob_page::Page>,
|
||||
pub icon_packs: Vec<IconPack>,
|
||||
pub inactive_button_color: DeserializableRGB8,
|
||||
pub active_button_color: DeserializableRGB8,
|
||||
pub inactive_button_color: SerializableRGB8,
|
||||
pub active_button_color: SerializableRGB8,
|
||||
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
||||
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<ImageFilter>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn validate(self) -> Result<Self> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ()> {
|
|||
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))
|
||||
}
|
||||
|
|
|
@ -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<ImageFilter>,
|
||||
|
@ -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<Self, Self::Err> {
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<Rect>, // 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<String> = 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SwitchState, IconDescriptor>,
|
||||
pub icon: HashMap<SwitchState, IconDescriptorString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ButtonConfig {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub icon: HashMap<ButtonState, IconDescriptor>,
|
||||
pub icon: HashMap<ButtonState, IconDescriptorString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||
|
|
|
@ -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<PlayPauseState, IconDescriptor>,
|
||||
pub icon: HashMap<PlayPauseState, IconDescriptorString>,
|
||||
#[serde(default)]
|
||||
pub action: PlayPauseAction,
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ pub enum PlayPauseAction {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct PreviousAndNextConfig {
|
||||
#[serde(default)]
|
||||
pub icon: HashMap<PreviousAndNextState, IconDescriptor>,
|
||||
pub icon: HashMap<PreviousAndNextState, IconDescriptorString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||
|
|
|
@ -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<ShuffleState, IconDescriptor>,
|
||||
pub icon: HashMap<ShuffleState, IconDescriptorString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RepeatConfig {
|
||||
#[serde(default)]
|
||||
pub icon: HashMap<RepeatState, IconDescriptor>,
|
||||
pub icon: HashMap<RepeatState, IconDescriptorString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||
|
|
|
@ -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<String>,
|
||||
pub scrolling: Option<ScrollingConfig>,
|
||||
pub keys: HashMap<UIntVec2, KeyConfig>,
|
||||
pub keys: HashMap<KeyPosition, KeyConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Page {
|
||||
pub id: String,
|
||||
pub scrolling: Option<ScrollingConfig>,
|
||||
pub keys: HashMap<UIntVec2, KeyConfig>,
|
||||
pub keys: HashMap<KeyPosition, KeyConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -46,7 +46,7 @@ pub enum ScrollingConfigAxis {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct KeyConfig {
|
||||
pub label: Option<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub icon: Option<IconDescriptorString>,
|
||||
#[serde(default)]
|
||||
pub mode: KeyModes,
|
||||
}
|
||||
|
|
|
@ -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<State, String>,
|
||||
#[serde(default)]
|
||||
pub icon: HashMap<State, IconDescriptor>,
|
||||
pub icon: HashMap<State, IconDescriptorString>,
|
||||
pub circle_indicator: Option<CircleIndicatorConfig>,
|
||||
pub bar_indicator: Option<BarIndicatorConfig>,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 <x> and <y> separated by an 'x'")]
|
||||
pub struct KeyPositionFromStrError {}
|
||||
|
||||
impl FromStr for KeyPosition {
|
||||
type Err = KeyPositionFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RGB8, RGBParsingError> {
|
|||
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<RGBA8, RGBParsingError> {
|
||||
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<RGBA8, RGBParsingError> {
|
|||
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<RGBA8, RGBParsingError> {
|
||||
// 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<Self, Self::Err> {
|
||||
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<Self, Self::Err> {
|
||||
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<Self, Self::Err> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum RendererCommand {
|
||||
SetButton,
|
||||
}
|
|
@ -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::<StateChangeCommand>(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<State> {
|
||||
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<State> {
|
|||
.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<model::config::Config>,
|
||||
device: LoupedeckDevice,
|
||||
events_receiver: Receiver<LoupedeckEvent>,
|
||||
commands_sender: Sender<StateChangeCommand>,
|
||||
commands_receiver: Receiver<StateChangeCommand>,
|
||||
) {
|
||||
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<StateChangeCommand>, 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
|
||||
}
|
||||
|
|
78
deckster/src/runner/state.rs
Normal file
78
deckster/src/runner/state.rs
Normal file
|
@ -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<String, KeyPage>,
|
||||
pub knob_pages_by_id: HashMap<String, KnobPage>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn mutate_key_for_command<R>(
|
||||
&mut self,
|
||||
command_name: &'static str,
|
||||
page_id: &String,
|
||||
key_position: &KeyPosition,
|
||||
mutator: Box<dyn FnOnce(&mut Key) -> R>,
|
||||
) -> Option<R> {
|
||||
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<KeyPosition, Key>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KnobPage {
|
||||
pub id: String,
|
||||
pub knobs_by_position: EnumMap<KnobPosition, Knob>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
|
@ -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<KeyPage>,
|
||||
pub active_knob_page: Rc<KnobPage>,
|
||||
pub key_pages_by_id: HashMap<String, Rc<KeyPage>>,
|
||||
pub knob_pages_by_id: HashMap<String, Rc<KnobPage>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyPage {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KnobPage {
|
||||
pub id: String,
|
||||
pub knobs_by_position: EnumMap<KnobPosition, Option<Knob>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Knob {
|
||||
pub id: KnobPosition,
|
||||
pub icon: IconDescriptor,
|
||||
pub label: String,
|
||||
pub value: f32,
|
||||
}
|
|
@ -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"
|
||||
rgb = "0.8.37"
|
||||
flume = "0.11.0"
|
|
@ -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
|
||||
|
|
|
@ -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<LoupedeckEvent>,
|
||||
commands_sender: crossbeam_channel::Sender<LoupedeckCommand>,
|
||||
events_receiver: flume::Receiver<LoupedeckEvent>,
|
||||
commands_sender: flume::Sender<LoupedeckCommand>,
|
||||
}
|
||||
|
||||
#[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<LoupedeckEvent> {
|
||||
pub fn events(&self) -> flume::Receiver<LoupedeckEvent> {
|
||||
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<LoupedeckDevice, ConnectError> {
|
||||
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("<unnamed>".to_owned()));
|
||||
|
||||
let (public_events_sender, public_events_receiver) = crossbeam_channel::unbounded::<LoupedeckEvent>();
|
||||
let (public_events_sender, public_events_receiver) = flume::unbounded::<LoupedeckEvent>();
|
||||
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::<LoupedeckCommand>();
|
||||
let (commands_sender, commands_receiver) = flume::unbounded::<LoupedeckCommand>();
|
||||
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<Box<dyn serialport::SerialPort>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ impl From<LoupedeckEvent> for ParseMessageResult {
|
|||
|
||||
pub(crate) fn read_messages_worker(
|
||||
mut port: Box<dyn SerialPort>,
|
||||
public_sender: crossbeam_channel::Sender<LoupedeckEvent>,
|
||||
public_sender: flume::Sender<LoupedeckEvent>,
|
||||
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
|
||||
) {
|
||||
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<dyn SerialPort>, receiver: crossbeam_channel::Receiver<LoupedeckCommand>) {
|
||||
pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: flume::Receiver<LoupedeckCommand>) {
|
||||
let mut next_transaction_id = 0;
|
||||
|
||||
let mut send = |command_id: u8, data: Bytes| -> Result<(), io::Error> {
|
||||
|
|
Loading…
Add table
Reference in a new issue