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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.3"
|
version = "0.20.3"
|
||||||
|
@ -320,7 +301,9 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"enum-map",
|
"enum-map",
|
||||||
|
"enum-ordinalize",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"flume",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"log",
|
"log",
|
||||||
"loupedeck_serial",
|
"loupedeck_serial",
|
||||||
|
@ -331,6 +314,7 @@ dependencies = [
|
||||||
"serde_regex",
|
"serde_regex",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
@ -445,12 +429,49 @@ dependencies = [
|
||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
|
@ -643,6 +664,16 @@ version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
|
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]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.20"
|
version = "0.4.20"
|
||||||
|
@ -654,9 +685,9 @@ name = "loupedeck_serial"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"crossbeam-channel",
|
|
||||||
"enum-ordinalize",
|
"enum-ordinalize",
|
||||||
"enumset",
|
"enumset",
|
||||||
|
"flume",
|
||||||
"rgb",
|
"rgb",
|
||||||
"serialport",
|
"serialport",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -692,6 +723,15 @@ dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nanorand"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
|
@ -712,6 +752,16 @@ dependencies = [
|
||||||
"autocfg",
|
"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]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.2"
|
version = "0.32.2"
|
||||||
|
@ -733,6 +783,29 @@ version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
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]]
|
[[package]]
|
||||||
name = "piet"
|
name = "piet"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -779,6 +852,15 @@ dependencies = [
|
||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.2"
|
version = "1.10.2"
|
||||||
|
@ -964,6 +1046,21 @@ dependencies = [
|
||||||
"lazy_static",
|
"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]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -1049,6 +1146,30 @@ dependencies = [
|
||||||
"time-core",
|
"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]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
|
@ -1212,6 +1333,12 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.89"
|
version = "0.2.89"
|
||||||
|
|
|
@ -7,7 +7,6 @@ edition = "2021"
|
||||||
color-eyre = "0.6.2"
|
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"
|
|
||||||
rgb = "0.8.37"
|
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"
|
||||||
|
@ -20,3 +19,7 @@ env_logger = "0.10.1"
|
||||||
clap = { version = "4.4.12", features = ["derive"] }
|
clap = { version = "4.4.12", features = ["derive"] }
|
||||||
enum-map = "3.0.0-beta.2"
|
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/*"]
|
key_pages = ["./key-pages/*"]
|
||||||
knob_pages = ["./knob-pages/*"]
|
knob_pages = ["./knob-pages/*"]
|
||||||
|
|
||||||
inactive_button_color = "#ff0000"
|
inactive_button_color = "#000060"
|
||||||
active_button_color = "#ffffff"
|
active_button_color = "#eeffff"
|
||||||
|
|
||||||
[buttons.0]
|
[buttons.0]
|
||||||
key_page = "default"
|
key_page = "default"
|
||||||
|
knob_page = "default"
|
||||||
|
|
||||||
[buttons.1]
|
[buttons.1]
|
||||||
key_page = "numpad"
|
key_page = "numpad"
|
||||||
|
knob_page = "default"
|
||||||
|
|
||||||
[buttons.2]
|
[buttons.2]
|
||||||
key_page = "emojis"
|
key_page = "emojis"
|
||||||
|
knob_page = "default"
|
||||||
|
|
||||||
[buttons.3]
|
[buttons.3]
|
||||||
key_page = "special_chars"
|
key_page = "special_chars"
|
||||||
|
knob_page = "default"
|
||||||
|
|
||||||
[initial]
|
[initial]
|
||||||
key_page = "default"
|
key_page = "default"
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use color_eyre::eyre::{OptionExt, WrapErr};
|
use color_eyre::eyre::WrapErr;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
@ -28,7 +28,8 @@ enum Command {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
#[tokio::main]
|
||||||
|
pub async fn main() -> Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
@ -66,9 +67,10 @@ pub fn main() -> Result<()> {
|
||||||
initial: deckster_file.initial,
|
initial: deckster_file.initial,
|
||||||
active_button_color: deckster_file.active_button_color,
|
active_button_color: deckster_file.active_button_color,
|
||||||
inactive_button_color: deckster_file.inactive_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 std::collections::HashMap;
|
||||||
|
|
||||||
|
use color_eyre::{eyre::eyre, Result};
|
||||||
use enum_map::EnumMap;
|
use enum_map::EnumMap;
|
||||||
use rgb::RGB8;
|
use rgb::RGB8;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model;
|
use crate::model;
|
||||||
use crate::model::image_filter::ImageFilter;
|
use crate::model::image_filter::ImageFilter;
|
||||||
use crate::model::rgb::DeserializableRGB8;
|
use crate::model::rgb::SerializableRGB8;
|
||||||
use crate::model::ButtonPosition;
|
use crate::model::ButtonPosition;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub icon_packs: Vec<IconPack>,
|
pub icon_packs: Vec<IconPack>,
|
||||||
#[serde(default = "inactive_button_color_default")]
|
#[serde(default = "inactive_button_color_default")]
|
||||||
pub inactive_button_color: DeserializableRGB8,
|
pub inactive_button_color: SerializableRGB8,
|
||||||
#[serde(default = "active_button_color_default")]
|
#[serde(default = "active_button_color_default")]
|
||||||
pub active_button_color: DeserializableRGB8,
|
pub active_button_color: SerializableRGB8,
|
||||||
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
|
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
|
||||||
pub initial: InitialConfig,
|
pub initial: InitialConfig,
|
||||||
}
|
}
|
||||||
|
@ -31,18 +32,18 @@ pub struct Config {
|
||||||
pub key_pages_by_id: HashMap<String, model::key_page::Page>,
|
pub key_pages_by_id: HashMap<String, model::key_page::Page>,
|
||||||
pub knob_pages_by_id: HashMap<String, model::knob_page::Page>,
|
pub knob_pages_by_id: HashMap<String, model::knob_page::Page>,
|
||||||
pub icon_packs: Vec<IconPack>,
|
pub icon_packs: Vec<IconPack>,
|
||||||
pub inactive_button_color: DeserializableRGB8,
|
pub inactive_button_color: SerializableRGB8,
|
||||||
pub active_button_color: DeserializableRGB8,
|
pub active_button_color: SerializableRGB8,
|
||||||
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
||||||
pub initial: InitialConfig,
|
pub initial: InitialConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inactive_button_color_default() -> DeserializableRGB8 {
|
fn inactive_button_color_default() -> SerializableRGB8 {
|
||||||
DeserializableRGB8(RGB8::new(128, 128, 128))
|
SerializableRGB8(RGB8::new(128, 128, 128))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_button_color_default() -> DeserializableRGB8 {
|
fn active_button_color_default() -> SerializableRGB8 {
|
||||||
DeserializableRGB8(RGB8::new(0, 255, 0))
|
SerializableRGB8(RGB8::new(0, 255, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default)]
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
@ -63,3 +64,43 @@ pub struct IconPack {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub global_filter: Option<ImageFilter>,
|
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 serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay, DeserializeFromStr, Eq, PartialEq, Hash)]
|
#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
|
||||||
pub struct UIntVec2 {
|
pub struct UIntVec2 {
|
||||||
x: u64,
|
x: u64,
|
||||||
y: 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())
|
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::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::DeserializeFromStr;
|
use serde_with::DeserializeFromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError};
|
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 struct IconDescriptor {
|
||||||
pub source: IconDescriptorSource,
|
pub source: IconDescriptorSource,
|
||||||
pub filter: Option<ImageFilter>,
|
pub filter: Option<ImageFilter>,
|
||||||
|
@ -24,7 +28,7 @@ pub enum IconDescriptorFromStrError {
|
||||||
MissingImageFilterClosingBracket,
|
MissingImageFilterClosingBracket,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for IconDescriptor {
|
impl FromStr for IconDescriptorString {
|
||||||
type Err = IconDescriptorFromStrError;
|
type Err = IconDescriptorFromStrError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
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)?)
|
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 {
|
pub enum IconDescriptorSource {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use piet::kurbo::Rect;
|
use piet::kurbo::Rect;
|
||||||
use rgb::RGB8;
|
use rgb::RGB8;
|
||||||
use serde_with::DeserializeFromStr;
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::model::geometry::parse_positive_rect_from_str;
|
use crate::model::geometry::{fmt_positive_rect, parse_positive_rect_from_str};
|
||||||
use crate::model::rgb::parse_rgb8_from_hex_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 struct ImageFilter {
|
||||||
pub crop_original: Option<Rect>, // applied before scale and rotate
|
pub crop_original: Option<Rect>, // applied before scale and rotate
|
||||||
pub scale: f32,
|
pub scale: f32,
|
||||||
|
@ -21,6 +22,24 @@ pub struct ImageFilter {
|
||||||
pub invert: bool,
|
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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ImageFilterFromStringError {
|
pub enum ImageFilterFromStringError {
|
||||||
#[error("Unknown filter: {name}")]
|
#[error("Unknown filter: {name}")]
|
||||||
|
@ -80,18 +99,7 @@ impl FromStr for ImageFilter {
|
||||||
|
|
||||||
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect();
|
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect();
|
||||||
|
|
||||||
let mut result = ImageFilter {
|
let mut result = ImageFilter::default();
|
||||||
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 previous_filter_names: Vec<String> = Vec::new();
|
let mut previous_filter_names: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for filter in filters {
|
for filter in filters {
|
||||||
|
@ -165,3 +173,89 @@ impl FromStr for ImageFilter {
|
||||||
Ok(result)
|
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 serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct SwitchConfig {
|
pub struct SwitchConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<SwitchState, IconDescriptor>,
|
pub icon: HashMap<SwitchState, IconDescriptorString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ButtonConfig {
|
pub struct ButtonConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<ButtonState, IconDescriptor>,
|
pub icon: HashMap<ButtonState, IconDescriptorString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -2,12 +2,12 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PlayPauseConfig {
|
pub struct PlayPauseConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PlayPauseState, IconDescriptor>,
|
pub icon: HashMap<PlayPauseState, IconDescriptorString>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub action: PlayPauseAction,
|
pub action: PlayPauseAction,
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ pub enum PlayPauseAction {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PreviousAndNextConfig {
|
pub struct PreviousAndNextConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PreviousAndNextState, IconDescriptor>,
|
pub icon: HashMap<PreviousAndNextState, IconDescriptorString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -2,18 +2,18 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ShuffleConfig {
|
pub struct ShuffleConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<ShuffleState, IconDescriptor>,
|
pub icon: HashMap<ShuffleState, IconDescriptorString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RepeatConfig {
|
pub struct RepeatConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<RepeatState, IconDescriptor>,
|
pub icon: HashMap<RepeatState, IconDescriptorString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -3,21 +3,21 @@ use std::collections::HashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::geometry::UIntVec2;
|
use crate::model::geometry::UIntVec2;
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
use crate::model::{key_modes, KnobPosition};
|
use crate::model::{key_modes, KeyPosition, KnobPosition};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
pub scrolling: Option<ScrollingConfig>,
|
pub scrolling: Option<ScrollingConfig>,
|
||||||
pub keys: HashMap<UIntVec2, KeyConfig>,
|
pub keys: HashMap<KeyPosition, KeyConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub scrolling: Option<ScrollingConfig>,
|
pub scrolling: Option<ScrollingConfig>,
|
||||||
pub keys: HashMap<UIntVec2, KeyConfig>,
|
pub keys: HashMap<KeyPosition, KeyConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -46,7 +46,7 @@ pub enum ScrollingConfigAxis {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KeyConfig {
|
pub struct KeyConfig {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub icon: Option<IconDescriptor>,
|
pub icon: Option<IconDescriptorString>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: KeyModes,
|
pub mode: KeyModes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ use std::num::NonZeroU8;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
use crate::model::rgb::DeserializableRGB8WithOptionalAlpha;
|
use crate::model::rgb::SerializableRGB8WithOptionalAlpha;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -22,7 +22,7 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: HashMap<State, String>,
|
pub label: HashMap<State, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<State, IconDescriptor>,
|
pub icon: HashMap<State, IconDescriptorString>,
|
||||||
pub circle_indicator: Option<CircleIndicatorConfig>,
|
pub circle_indicator: Option<CircleIndicatorConfig>,
|
||||||
pub bar_indicator: Option<BarIndicatorConfig>,
|
pub bar_indicator: Option<BarIndicatorConfig>,
|
||||||
}
|
}
|
||||||
|
@ -54,12 +54,12 @@ pub enum State {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct CircleIndicatorConfig {
|
pub struct CircleIndicatorConfig {
|
||||||
pub color: DeserializableRGB8WithOptionalAlpha,
|
pub color: SerializableRGB8WithOptionalAlpha,
|
||||||
pub width: NonZeroU8,
|
pub width: NonZeroU8,
|
||||||
pub radius: u8,
|
pub radius: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BarIndicatorConfig {
|
pub struct BarIndicatorConfig {
|
||||||
pub color: DeserializableRGB8WithOptionalAlpha,
|
pub color: SerializableRGB8WithOptionalAlpha,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ use std::collections::HashMap;
|
||||||
use enum_map::EnumMap;
|
use enum_map::EnumMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptorString;
|
||||||
use crate::model::rgb::DeserializableRGB8WithOptionalAlpha;
|
use crate::model::rgb::SerializableRGB8WithOptionalAlpha;
|
||||||
use crate::model::{knob_modes, KnobPosition};
|
use crate::model::{knob_modes, KnobPosition};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -24,7 +24,7 @@ pub struct Knob {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: String,
|
pub label: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: IconDescriptor,
|
pub icon: IconDescriptorString,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub indicator: KnobIndicators,
|
pub indicator: KnobIndicators,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -39,12 +39,12 @@ pub struct KnobIndicators {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KnobIndicatorBarConfig {
|
pub struct KnobIndicatorBarConfig {
|
||||||
pub color: DeserializableRGB8WithOptionalAlpha,
|
pub color: SerializableRGB8WithOptionalAlpha,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KnobIndicatorCircleConfig {
|
pub struct KnobIndicatorCircleConfig {
|
||||||
pub color: DeserializableRGB8WithOptionalAlpha,
|
pub color: SerializableRGB8WithOptionalAlpha,
|
||||||
pub width: u8,
|
pub width: u8,
|
||||||
pub radius: u8,
|
pub radius: u8,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use enum_map::Enum;
|
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 config;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
|
@ -11,7 +18,41 @@ pub mod knob_modes;
|
||||||
pub mod knob_page;
|
pub mod knob_page;
|
||||||
pub mod rgb;
|
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")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum KnobPosition {
|
pub enum KnobPosition {
|
||||||
LeftTop,
|
LeftTop,
|
||||||
|
@ -41,3 +82,33 @@ pub enum ButtonPosition {
|
||||||
#[serde(rename = "7")]
|
#[serde(rename = "7")]
|
||||||
N7,
|
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 std::str::FromStr;
|
||||||
|
|
||||||
use rgb::{RGB8, RGBA8};
|
use rgb::{RGB8, RGBA8};
|
||||||
use serde_with::DeserializeFromStr;
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -21,6 +22,10 @@ pub fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, RGBParsingError> {
|
||||||
Err(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> {
|
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 {
|
||||||
|
@ -35,6 +40,10 @@ pub fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, RGBParsingError> {
|
||||||
Err(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> {
|
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() {
|
||||||
|
@ -44,35 +53,53 @@ pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, DeserializeFromStr)]
|
#[derive(Debug, SerializeDisplay, DeserializeFromStr)]
|
||||||
pub struct DeserializableRGB8(pub RGB8);
|
pub struct SerializableRGB8(pub RGB8);
|
||||||
|
|
||||||
impl FromStr for DeserializableRGB8 {
|
impl FromStr for SerializableRGB8 {
|
||||||
type Err = RGBParsingError;
|
type Err = RGBParsingError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
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)]
|
impl Display for SerializableRGB8 {
|
||||||
pub struct DeserializableRGBA8(pub RGBA8);
|
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;
|
type Err = RGBParsingError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
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)]
|
impl Display for SerializableRGBA8 {
|
||||||
pub struct DeserializableRGB8WithOptionalAlpha(pub RGBA8);
|
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;
|
type Err = RGBParsingError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
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::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use color_eyre::eyre::{ContextCompat, WrapErr};
|
use color_eyre::eyre::{ContextCompat, WrapErr};
|
||||||
use color_eyre::Result;
|
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::commands::VibrationPattern;
|
||||||
use loupedeck_serial::device::LoupedeckDevice;
|
use loupedeck_serial::device::LoupedeckDevice;
|
||||||
use state::State;
|
use loupedeck_serial::events::LoupedeckEvent;
|
||||||
|
|
||||||
use crate::model;
|
use crate::model;
|
||||||
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
use crate::model::ButtonPosition;
|
||||||
|
use crate::runner::state::{State, StateChangeCommand};
|
||||||
|
|
||||||
mod command;
|
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
pub fn start(config: model::config::Config) -> Result<()> {
|
pub async fn start(config: model::config::Config) -> Result<()> {
|
||||||
let state = create_state(&config)?;
|
let config = Arc::new(config);
|
||||||
|
|
||||||
let device = LoupedeckDevice::discover()?
|
let device = LoupedeckDevice::discover()?
|
||||||
.first()
|
.first()
|
||||||
.wrap_err("No device connected.")?
|
.wrap_err("No device connected.")?
|
||||||
|
@ -24,15 +32,41 @@ pub fn start(config: model::config::Config) -> Result<()> {
|
||||||
|
|
||||||
device.vibrate(VibrationPattern::RiseFall);
|
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(())
|
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
|
let key_pages_by_id: HashMap<_, _> = config
|
||||||
.key_pages_by_id
|
.key_pages_by_id
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, p)| state::KeyPage { id: id.clone() })
|
.map(|(id, p)| state::KeyPage {
|
||||||
.map(|p| (p.id.clone(), Rc::new(p)))
|
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();
|
.collect();
|
||||||
|
|
||||||
let knob_pages_by_id: HashMap<_, _> = config
|
let knob_pages_by_id: HashMap<_, _> = config
|
||||||
|
@ -40,33 +74,143 @@ fn create_state(config: &model::config::Config) -> Result<State> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, p)| state::KnobPage {
|
.map(|(id, p)| state::KnobPage {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
knobs_by_position: p
|
knobs_by_position: EnumMap::from_fn(|position| {
|
||||||
.knobs
|
let knob_config = &p.knobs[position];
|
||||||
.iter()
|
|
||||||
.map(|(p, k)| state::Knob {
|
state::Knob {
|
||||||
id: p,
|
position,
|
||||||
label: k.label.clone(),
|
label: knob_config.label.clone(),
|
||||||
icon: k.icon.clone(),
|
icon: knob_config.icon.0.clone(),
|
||||||
value: 0.0,
|
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();
|
.collect();
|
||||||
|
|
||||||
Ok(State {
|
State {
|
||||||
active_key_page: Rc::clone(
|
active_key_page_id: config.initial.key_page.clone(),
|
||||||
key_pages_by_id
|
active_knob_page_id: config.initial.knob_page.clone(),
|
||||||
.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))?,
|
|
||||||
),
|
|
||||||
key_pages_by_id,
|
key_pages_by_id,
|
||||||
knob_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"
|
enumset = "1.1.3"
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
thiserror = "1.0.52"
|
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)]
|
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LoupedeckKnob {
|
pub enum LoupedeckKnob {
|
||||||
KnobTopLeft = 0x01,
|
KnobLeftTop = 0x01,
|
||||||
KnobCenterLeft = 0x02,
|
KnobLeftMiddle = 0x02,
|
||||||
KnobBottomLeft = 0x03,
|
KnobLeftBottom = 0x03,
|
||||||
KnobTopRight = 0x04,
|
KnobRightTop = 0x04,
|
||||||
KnobCenterRight = 0x05,
|
KnobRightMiddle = 0x05,
|
||||||
KnobBottomRight = 0x06,
|
KnobRightBottom = 0x06,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Ordinalize, EnumSetType)]
|
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LoupedeckButton {
|
pub enum LoupedeckButton {
|
||||||
KnobLeftTop = 0x01,
|
|
||||||
KnobLeftCenter = 0x02,
|
|
||||||
KnobLeftBottom = 0x03,
|
|
||||||
KnobRightTop = 0x04,
|
|
||||||
KnobRightCenter = 0x05,
|
|
||||||
KnobRightBottom = 0x06,
|
|
||||||
N0 = 0x07,
|
N0 = 0x07,
|
||||||
N1 = 0x08,
|
N1 = 0x08,
|
||||||
N2 = 0x09,
|
N2 = 0x09,
|
||||||
|
@ -33,12 +27,6 @@ pub enum LoupedeckButton {
|
||||||
N7 = 0x0e,
|
N7 = 0x0e,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoupedeckButton {
|
|
||||||
pub fn supports_color(&self) -> bool {
|
|
||||||
self.ordinal() >= LoupedeckButton::N0.ordinal() && self.ordinal() <= LoupedeckButton::N7.ordinal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct LoupedeckDeviceDisplayConfiguration {
|
pub struct LoupedeckDeviceDisplayConfiguration {
|
||||||
|
@ -117,21 +105,15 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
|
||||||
product_id: 0x0004,
|
product_id: 0x0004,
|
||||||
name: "Loupedeck Live",
|
name: "Loupedeck Live",
|
||||||
available_knobs: enum_set!(
|
available_knobs: enum_set!(
|
||||||
LoupedeckKnob::KnobTopLeft
|
LoupedeckKnob::KnobLeftTop
|
||||||
| LoupedeckKnob::KnobCenterLeft
|
| LoupedeckKnob::KnobLeftMiddle
|
||||||
| LoupedeckKnob::KnobBottomLeft
|
| LoupedeckKnob::KnobLeftBottom
|
||||||
| LoupedeckKnob::KnobTopRight
|
| LoupedeckKnob::KnobRightTop
|
||||||
| LoupedeckKnob::KnobCenterRight
|
| LoupedeckKnob::KnobRightMiddle
|
||||||
| LoupedeckKnob::KnobBottomRight
|
| LoupedeckKnob::KnobRightBottom
|
||||||
),
|
),
|
||||||
available_buttons: enum_set!(
|
available_buttons: enum_set!(
|
||||||
LoupedeckButton::KnobLeftTop
|
LoupedeckButton::N0
|
||||||
| LoupedeckButton::KnobLeftCenter
|
|
||||||
| LoupedeckButton::KnobLeftBottom
|
|
||||||
| LoupedeckButton::KnobRightTop
|
|
||||||
| LoupedeckButton::KnobRightCenter
|
|
||||||
| LoupedeckButton::KnobRightBottom
|
|
||||||
| LoupedeckButton::N0
|
|
||||||
| LoupedeckButton::N1
|
| LoupedeckButton::N1
|
||||||
| LoupedeckButton::N2
|
| LoupedeckButton::N2
|
||||||
| LoupedeckButton::N3
|
| 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::io::{Read, Write};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use rgb::{ComponentSlice, RGB8};
|
||||||
|
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
|
||||||
use thiserror::Error;
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct AvailableLoupedeckDevice {
|
pub struct AvailableLoupedeckDevice {
|
||||||
pub(crate) port_name: String,
|
pub(crate) port_name: String,
|
||||||
|
@ -38,8 +40,8 @@ pub struct LoupedeckDevice {
|
||||||
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
|
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
|
||||||
pub(crate) serial_number: String,
|
pub(crate) serial_number: String,
|
||||||
pub(crate) firmware_version: String,
|
pub(crate) firmware_version: String,
|
||||||
events_receiver: crossbeam_channel::Receiver<LoupedeckEvent>,
|
events_receiver: flume::Receiver<LoupedeckEvent>,
|
||||||
commands_sender: crossbeam_channel::Sender<LoupedeckCommand>,
|
commands_sender: flume::Sender<LoupedeckCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -70,9 +72,6 @@ pub enum RefreshDisplayError {
|
||||||
pub enum SetButtonColorError {
|
pub enum SetButtonColorError {
|
||||||
#[error("The specified button is not available for this device.")]
|
#[error("The specified button is not available for this device.")]
|
||||||
UnknownButton,
|
UnknownButton,
|
||||||
|
|
||||||
#[error("The button does not allow setting a color.")]
|
|
||||||
ColorNotSupported,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -100,7 +99,7 @@ impl LoupedeckDevice {
|
||||||
&self.firmware_version
|
&self.firmware_version
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn events_channel(&self) -> crossbeam_channel::Receiver<LoupedeckEvent> {
|
pub fn events(&self) -> flume::Receiver<LoupedeckEvent> {
|
||||||
self.events_receiver.clone()
|
self.events_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,10 +112,6 @@ impl LoupedeckDevice {
|
||||||
return Err(SetButtonColorError::UnknownButton);
|
return Err(SetButtonColorError::UnknownButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !button.supports_color() {
|
|
||||||
return Err(SetButtonColorError::ColorNotSupported);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The write worker thread not running means the device was disconnected.
|
// The write worker thread not running means the device was disconnected.
|
||||||
// In that case, the read worker thread sends a LoupedeckEvent::Disconnected.
|
// In that case, the read worker thread sends a LoupedeckEvent::Disconnected.
|
||||||
self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).ok();
|
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> {
|
pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result<LoupedeckDevice, ConnectError> {
|
||||||
let mut port = serialport::new(port_name, 256000)
|
let mut port = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?;
|
||||||
.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 buf = [0; WS_UPGRADE_RESPONSE_START.len()];
|
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() {
|
if buf != WS_UPGRADE_RESPONSE_START.as_bytes() {
|
||||||
return Err(ConnectError::WrongEarlyHandshakeResponse);
|
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));
|
sleep(Duration::from_secs(1));
|
||||||
port.clear(ClearBuffer::Input)?;
|
port.clear(ClearBuffer::Input)?;
|
||||||
let cloned_port = port.try_clone().expect("port must be cloneable");
|
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 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);
|
let (internal_events_sender, internal_events_receiver) = mpsc::sync_channel(2);
|
||||||
thread::Builder::new().name(thread_name_base.to_owned() + " read worker").spawn(move || {
|
thread::Builder::new().name(thread_name_base.to_owned() + " read worker").spawn(move || {
|
||||||
read_messages_worker(port, public_events_sender, internal_events_sender);
|
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 || {
|
thread::Builder::new().name(thread_name_base.to_owned() + " write worker").spawn(move || {
|
||||||
write_messages_worker(cloned_port, commands_receiver);
|
write_messages_worker(cloned_port, commands_receiver);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
commands_sender.send(LoupedeckCommand::RequestSerialNumber).unwrap();
|
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),
|
Ok(LoupedeckInternalEvent::GetSerialNumberResponse { serial_number }) => Ok(serial_number),
|
||||||
_ => Err(ConnectError::WrongLateHandshakeResponse),
|
_ => Err(ConnectError::WrongLateHandshakeResponse),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
commands_sender.send(LoupedeckCommand::RequestFirmwareVersion).unwrap();
|
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),
|
Ok(LoupedeckInternalEvent::GetFirmwareVersionResponse { firmware_version }) => Ok(firmware_version),
|
||||||
_ => Err(ConnectError::WrongLateHandshakeResponse),
|
_ => Err(ConnectError::WrongLateHandshakeResponse),
|
||||||
}?;
|
}?;
|
||||||
|
@ -278,4 +269,20 @@ impl LoupedeckDevice {
|
||||||
commands_sender,
|
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,
|
Disconnected,
|
||||||
ButtonDown { button: LoupedeckButton },
|
ButtonDown { button: LoupedeckButton },
|
||||||
ButtonUp { button: LoupedeckButton },
|
ButtonUp { button: LoupedeckButton },
|
||||||
|
KnobDown { knob: LoupedeckKnob },
|
||||||
|
KnobUp { knob: LoupedeckKnob },
|
||||||
KnobRotate { knob: LoupedeckKnob, direction: RotationDirection },
|
KnobRotate { knob: LoupedeckKnob, direction: RotationDirection },
|
||||||
Touch { touch_id: u8, x: u16, y: u16, is_end: bool },
|
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(
|
pub(crate) fn read_messages_worker(
|
||||||
mut port: Box<dyn SerialPort>,
|
mut port: Box<dyn SerialPort>,
|
||||||
public_sender: crossbeam_channel::Sender<LoupedeckEvent>,
|
public_sender: flume::Sender<LoupedeckEvent>,
|
||||||
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
|
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
|
||||||
) {
|
) {
|
||||||
let mut internal_sender = Some(internal_sender);
|
let mut internal_sender = Some(internal_sender);
|
||||||
|
@ -58,14 +58,23 @@ pub(crate) fn read_messages_worker(
|
||||||
|
|
||||||
while !should_stop {
|
while !should_stop {
|
||||||
let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH);
|
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 {
|
let read_length = match read_result {
|
||||||
// This fails only if the other side is disconnected.
|
Ok(length) => length,
|
||||||
// In that case, this thread should terminate anyway and we can ignore the error.
|
Err(err) => {
|
||||||
public_sender.send(LoupedeckEvent::Disconnected).ok();
|
match err.kind() {
|
||||||
break;
|
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);
|
chunk.truncate(read_length);
|
||||||
buffer.put(chunk);
|
buffer.put(chunk);
|
||||||
|
@ -120,18 +129,68 @@ 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 => {
|
0x00 => match message[1] {
|
||||||
// Button
|
0x00 => match message[0] {
|
||||||
let button = LoupedeckButton::from_ordinal(message[0]).expect("Invalid button ID");
|
0x01 => LoupedeckEvent::KnobDown {
|
||||||
|
knob: LoupedeckKnob::KnobLeftTop,
|
||||||
match message[1] {
|
},
|
||||||
0x00 => LoupedeckEvent::ButtonDown { button },
|
0x02 => LoupedeckEvent::KnobDown {
|
||||||
_ => LoupedeckEvent::ButtonUp { button },
|
knob: LoupedeckKnob::KnobLeftMiddle,
|
||||||
}
|
},
|
||||||
.into()
|
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 => {
|
0x01 => {
|
||||||
// Knob
|
|
||||||
let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID");
|
let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID");
|
||||||
|
|
||||||
LoupedeckEvent::KnobRotate {
|
LoupedeckEvent::KnobRotate {
|
||||||
|
@ -149,7 +208,6 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
0x4d | 0x6d => {
|
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();
|
||||||
|
@ -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 next_transaction_id = 0;
|
||||||
|
|
||||||
let mut send = |command_id: u8, data: Bytes| -> Result<(), io::Error> {
|
let mut send = |command_id: u8, data: Bytes| -> Result<(), io::Error> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue