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