This commit is contained in:
Moritz Ruth 2024-01-02 20:18:47 +01:00
parent 82077a6e1e
commit 474ecf4f5e
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
25 changed files with 864 additions and 256 deletions

167
Cargo.lock generated
View file

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

View file

@ -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"
@ -20,3 +19,7 @@ env_logger = "0.10.1"
clap = { version = "4.4.12", features = ["derive"] }
enum-map = "3.0.0-beta.2"
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"

View file

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

View file

@ -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?
}
};

View file

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

View file

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

View file

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

View file

@ -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(())
}
}

View file

@ -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)]

View file

@ -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)]

View file

@ -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)]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum RendererCommand {
SetButton,
}

View file

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

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

View file

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

View file

@ -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"
flume = "0.11.0"

View file

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

View file

@ -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 dont know why. There is garbage in the buffer without this.
// I dont 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)
}
}

View file

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

View file

@ -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> {