This commit is contained in:
Moritz Ruth 2024-01-31 01:23:56 +01:00
parent 1904e3e96a
commit b5a7ab3c6b
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
71 changed files with 921 additions and 1297 deletions

View file

@ -0,0 +1,12 @@
[package]
name = "deckster_mode"
version = "0.1.0"
edition = "2021"
[dependencies]
deckster_shared = { path = "../deckster_shared" }
thiserror = "1.0.56"
im = "15.1.0"
either = "1.9.0"
serde = { version = "1.0.196", default-features = false }
serde_json = "1.0.113"

View file

@ -0,0 +1,88 @@
use std::any::TypeId;
use std::io;
use std::io::BufRead;
use either::Either;
use serde::de::DeserializeOwned;
use thiserror::Error;
use deckster_shared::handler_communication::HandlerInitializationResultMessage;
pub use deckster_shared::handler_communication::{HandlerEvent, HandlerInitializationError, InitialHandlerMessage};
pub use deckster_shared::path::*;
pub use deckster_shared::state::{KeyStyleByStateMap, KnobStyleByStateMap};
#[derive(Debug, Error)]
pub enum RunError {
#[error("A stdin line could not be read.")]
LineIo(#[from] io::Error),
#[error("A stdin line could not be deserialized: {description}")]
LineDeserialization { line: String, description: String },
}
pub trait DecksterHandler {
fn handle(&mut self, event: HandlerEvent);
}
pub fn run<
KeyConfig: Clone + DeserializeOwned + 'static,
KnobConfig: Clone + DeserializeOwned + 'static,
H: DecksterHandler,
I: FnOnce(InitialHandlerMessage<KeyConfig, KnobConfig>) -> Result<H, HandlerInitializationError>,
>(
init_handler: I,
) -> Result<(), RunError> {
let mut handler: Either<H, I> = Either::Right(init_handler);
let supports_keys = TypeId::of::<KeyConfig>() != TypeId::of::<()>();
let supports_knobs = TypeId::of::<KnobConfig>() != TypeId::of::<()>();
let handle = io::stdin().lock();
for line in handle.lines() {
let line = line?;
match handler {
Either::Left(mut h) => {
let event: HandlerEvent = serde_json::from_str(&line).map_err(|e| RunError::LineDeserialization {
line,
description: e.to_string(),
})?;
h.handle(event);
handler = Either::Left(h);
}
Either::Right(init_handler) => {
let initial_message = serde_json::from_str::<InitialHandlerMessage<KeyConfig, KnobConfig>>(&line);
match initial_message {
Ok(initial_message) => match init_handler(initial_message) {
Ok(h) => {
println!("{}", serde_json::to_string(&HandlerInitializationResultMessage::Ready).unwrap());
handler = Either::Left(h)
}
Err(error) => {
println!("{}", serde_json::to_string(&HandlerInitializationResultMessage::Error { error }).unwrap());
return Ok(());
}
},
Err(err) => {
println!(
"{}",
serde_json::to_string(&HandlerInitializationResultMessage::Error {
error: HandlerInitializationError::InvalidConfig {
supports_keys,
supports_knobs,
message: err.to_string().into_boxed_str(),
}
})
.unwrap()
);
return Ok(());
}
}
}
}
}
Ok(())
}

View file

@ -0,0 +1,15 @@
[package]
name = "deckster_shared"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.195", features = ["derive", "rc"] }
serde_with = "3.4.0"
thiserror = "1.0.56"
derive_more = "0.99.17"
rgb = "0.8.37"
enum-ordinalize = "4.3.0"
enum-map = "3.0.0-beta.2"
im = { version = "15.1.0", features = ["serde"] }
parse-display = "0.8.2"

View file

@ -0,0 +1,77 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::path::{KeyPath, KnobPath};
use crate::style::{KeyStyle, KnobStyle};
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum RotationDirection {
Clockwise,
Counterclockwise,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KnobEvent {
Press,
ButtonDown,
ButtonUp,
Rotate { direction: RotationDirection },
VisibilityChange { is_visible: bool },
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum KeyTouchEventKind {
Start,
Move,
End,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KeyEvent {
Press,
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
VisibilityChange { is_visible: bool },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum HandlerEvent {
Knob { path: KnobPath, event: KnobEvent },
Key { path: KeyPath, event: KeyEvent },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "kebab-case")]
pub enum HandlerCommand {
SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
SetKnobValue { path: KnobPath, value: Option<f32> },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct InitialHandlerMessage<KeyConfig: Clone, KnobConfig: Clone> {
pub key_configs: im::HashMap<KeyPath, (Box<str>, KeyConfig)>,
pub knob_configs: im::HashMap<KnobPath, (Box<str>, KnobConfig)>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum HandlerInitializationResultMessage {
Ready,
Error { error: HandlerInitializationError },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Error)]
pub enum HandlerInitializationError {
#[error("The provided mode string is invalid: {message}")]
InvalidModeString { message: Box<str> },
#[error("The provided handler config is invalid: {message}")]
InvalidConfig {
supports_keys: bool,
supports_knobs: bool,
message: Box<str>,
},
#[error("{message}")]
Other { message: Box<str> },
}

View file

@ -0,0 +1,89 @@
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::image_filter::{ImageFilter, ImageFilterFromStringError};
#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
pub struct IconDescriptor {
pub source: IconDescriptorSource,
pub filter: ImageFilter,
}
#[derive(Debug, thiserror::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 let Some(raw_source) = raw_source.strip_prefix('@') {
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: ImageFilter = if raw_filter.is_empty() {
ImageFilter::default()
} 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);
}
ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?
};
Ok(IconDescriptor { source, filter })
}
}
impl Display for IconDescriptor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}[{}]", self.source, self.filter))
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub enum IconDescriptorSource {
IconPack { pack_id: String, icon_id: String },
Path(PathBuf),
}
impl IconDescriptorSource {
pub fn pack_id(&self) -> Option<&String> {
match self {
IconDescriptorSource::IconPack { pack_id, .. } => Some(pack_id),
_ => None,
}
}
}
impl Display for IconDescriptorSource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
IconDescriptorSource::IconPack { pack_id, icon_id } => f.write_fmt(format_args!("@{}/{}", pack_id, icon_id)),
IconDescriptorSource::Path(path) => f.write_str(&path.to_string_lossy()),
}
}
}

View file

@ -0,0 +1,282 @@
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
use crate::rgb::RGB8Wrapper;
#[derive(Debug, PartialEq, Clone)]
pub struct ImageFilterTransform {
pub scale: f32,
/// Must be in 0..=3
pub clockwise_quarter_rotations: u8,
pub alpha: f32,
}
impl ImageFilterTransform {
pub fn merge_over(&self, other: &ImageFilterTransform) -> ImageFilterTransform {
ImageFilterTransform {
scale: self.scale * other.scale,
alpha: self.alpha * other.alpha,
clockwise_quarter_rotations: (self.clockwise_quarter_rotations + other.clockwise_quarter_rotations) % 4,
}
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct ImageFilterDestructive {
pub color: Option<RGB8Wrapper>,
pub grayscale: bool,
pub invert: bool,
}
impl ImageFilterDestructive {
pub fn merge_over(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive {
ImageFilterDestructive {
color: self.color.or(other.color),
grayscale: self.grayscale || other.grayscale,
invert: self.invert ^ other.invert,
}
}
}
#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)]
pub struct ImageFilter {
pub transform: ImageFilterTransform,
pub destructive: ImageFilterDestructive,
}
impl ImageFilter {
pub fn merge_over(&self, other: &ImageFilter) -> ImageFilter {
ImageFilter {
transform: self.transform.merge_over(&other.transform),
destructive: self.destructive.merge_over(&other.destructive),
}
}
}
impl Eq for ImageFilter {
fn assert_receiver_is_total_eq(&self) {}
}
impl Hash for ImageFilter {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.to_string().hash(hasher)
}
}
impl Default for ImageFilter {
fn default() -> Self {
DEFAULT_IMAGE_FILTER.clone()
}
}
const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter {
transform: ImageFilterTransform {
scale: 1.0,
clockwise_quarter_rotations: 0,
alpha: 1.0,
},
destructive: ImageFilterDestructive {
color: None,
grayscale: false,
invert: false,
},
};
#[derive(Debug, Error)]
pub enum ImageFilterFromStringError {
#[error("Unknown filter: {name}")]
UnknownFilter { name: String },
#[error("Filter {name} can only be used once.")]
FilterUsedMoreThanOnce { name: String },
#[error("Only the following values are allowed for rotate: -90, 90, 180, 270")]
RotationNotAllowed,
#[error("Filter {filter_name} requires a value ({filter_name}=<value>).")]
FilterValueMissing { filter_name: String },
#[error("Filter {filter_name} does not require or allow a value.")]
FilterValueIgnored { filter_name: String },
#[error("The following value supplied to {filter_name} could not be parsed: {raw_value}")]
FilterValueNotParsable { filter_name: String, raw_value: String },
#[error("{value} is not in the range ({range}) required by {filter_name}.")]
FilterValueNotInRange { filter_name: String, value: String, range: String },
}
impl FromStr for ImageFilter {
type Err = ImageFilterFromStringError;
fn from_str<'a>(s: &str) -> Result<Self, Self::Err> {
fn parse_f32_filter_value(
filter_name: &str,
raw_value: String,
range_string: &str,
check_range: Box<dyn Fn(&f32) -> bool>,
) -> Result<f32, ImageFilterFromStringError> {
let value = f32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?;
if !check_range(&value) {
return Err(ImageFilterFromStringError::FilterValueNotInRange {
filter_name: filter_name.to_string(),
range: range_string.to_string(),
value: value.to_string(),
});
}
Ok(value)
}
let mut result = ImageFilter::default();
let mut previous_filter_names: Vec<String> = Vec::new();
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).filter(|s| !s.is_empty()).collect();
if filters.is_empty() {
return Ok(result);
}
for filter in filters {
let split_filter = filter.split_once('=');
let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter {
(filter_name.to_owned(), Some(raw_value.to_owned()))
} else {
(filter.to_owned(), None)
};
if previous_filter_names.contains(&filter_name) {
return Err(ImageFilterFromStringError::FilterUsedMoreThanOnce { name: filter_name });
} else {
previous_filter_names.push(filter_name.clone());
}
let value_is_present = optional_raw_value.is_some();
let use_bool_value = || -> Result<bool, ImageFilterFromStringError> {
if value_is_present {
Err(ImageFilterFromStringError::FilterValueIgnored {
filter_name: filter_name.clone(),
})
} else {
Ok(true)
}
};
let use_raw_value = || -> Result<String, ImageFilterFromStringError> {
optional_raw_value.ok_or_else(|| ImageFilterFromStringError::FilterValueMissing {
filter_name: filter_name.clone(),
})
};
match filter_name.as_str() {
"scale" => result.transform.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
"rotate" => {
let raw_value = use_raw_value()?;
let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?;
result.transform.clockwise_quarter_rotations = match value {
0 => 0,
90 => 1,
180 => 2,
270 | -90 => 3,
_ => return Err(ImageFilterFromStringError::RotationNotAllowed),
};
}
"alpha" => result.transform.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?,
"color" => {
let raw_value = use_raw_value()?;
result.destructive.color = Some(
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?,
)
}
"grayscale" => result.destructive.grayscale = use_bool_value()?,
"invert" => result.destructive.invert = use_bool_value()?,
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
};
}
Ok(result)
}
}
impl Display for ImageFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut is_first = true;
if self.transform.scale != DEFAULT_IMAGE_FILTER.transform.scale {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("scale={}", self.transform.scale))?;
is_first = false;
}
if self.transform.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.transform.clockwise_quarter_rotations {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("rotate={}", self.transform.clockwise_quarter_rotations as u16 * 90))?;
is_first = false;
}
if self.transform.alpha != DEFAULT_IMAGE_FILTER.transform.alpha {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("alpha={}", self.transform.alpha))?;
is_first = false;
}
if let Some(color) = self.destructive.color {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("color={}", color))?;
is_first = false;
}
if self.destructive.grayscale {
if !is_first {
f.write_str("|")?
}
f.write_str("grayscale")?;
is_first = false;
}
if self.destructive.invert {
if !is_first {
f.write_str("|")?
}
f.write_str("invert")?;
is_first = false;
}
if let Some(color) = self.destructive.color {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("color={}", color))?;
// is_first = false;
}
Ok(())
}
}

View file

@ -0,0 +1,7 @@
pub mod handler_communication;
pub mod icon_descriptor;
pub mod image_filter;
pub mod path;
pub mod rgb;
pub mod state;
pub mod style;

View file

@ -0,0 +1,43 @@
use enum_map::Enum;
use enum_ordinalize::Ordinalize;
use parse_display::{Display, FromStr};
use serde_with::{DeserializeFromStr, SerializeDisplay};
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
/// One-based coordinates of a specific virtual (not physical) key.
#[display("{x}x{y}")]
pub struct KeyPosition {
pub x: u16,
pub y: u16,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
#[display("{page_id}/{position}")]
pub struct KeyPath {
pub page_id: String,
pub position: KeyPosition,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Enum, Ordinalize, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
#[display(style = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
}
impl KnobPosition {
pub fn is_left(&self) -> bool {
matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop)
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
#[display("{page_id}/{position}")]
pub struct KnobPath {
pub page_id: String,
pub position: KnobPosition,
}

View file

@ -0,0 +1,108 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use derive_more::{Deref, From, Into};
use rgb::{RGB8, RGBA8};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("The input value does not match the required format.")]
pub struct ParsingError {}
fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, ParsingError> {
let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 6 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ParsingError {})?;
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ParsingError {})?;
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ParsingError {})?;
return Ok(RGB8::new(r, g, b));
}
Err(ParsingError {})
}
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))
}
fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ParsingError> {
let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 8 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ParsingError {})?;
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ParsingError {})?;
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ParsingError {})?;
let a = u8::from_str_radix(&s[(first_index + 6)..(first_index + 8)], 16).map_err(|_| ParsingError {})?;
return Ok(RGBA8::new(r, g, b, a));
}
Err(ParsingError {})
}
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))
}
fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result<RGBA8, ParsingError> {
// optionally +1 for the '#'
match s.len() {
6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)),
8 | 9 => Ok(parse_rgba8_from_hex_str(s)?),
_ => Err(ParsingError {}),
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
pub struct RGB8Wrapper(RGB8);
impl FromStr for RGB8Wrapper {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_rgb8_from_hex_str(s).map(RGB8Wrapper)
}
}
impl Display for RGB8Wrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
fmt_rgb8_as_hex_string(&self.0, f)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
#[repr(transparent)]
pub struct RGBA8Wrapper(pub RGBA8);
impl FromStr for RGBA8Wrapper {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_rgba8_from_hex_str(s).map(RGBA8Wrapper)
}
}
impl Display for RGBA8Wrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
fmt_rgba8_as_hex_string(&self.0, f)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
#[repr(transparent)]
pub struct RGB8WithOptionalA(RGBA8);
impl FromStr for RGB8WithOptionalA {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(RGB8WithOptionalA)
}
}
impl Display for RGB8WithOptionalA {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
fmt_rgba8_as_hex_string(&self.0, f)
}
}

View file

@ -0,0 +1,22 @@
use std::collections::HashMap;
use crate::path::{KeyPath, KnobPath};
use crate::style::{KeyStyle, KnobStyle};
#[derive(Debug)]
pub struct Key {
pub path: KeyPath,
pub base_style: KeyStyle,
pub style: Option<KeyStyle>,
}
#[derive(Debug)]
pub struct Knob {
pub path: KnobPath,
pub base_style: KnobStyle,
pub style: Option<KnobStyle>,
pub value: Option<f32>,
}
pub type KeyStyleByStateMap<State> = HashMap<State, KeyStyle>;
pub type KnobStyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -0,0 +1,101 @@
use serde::{Deserialize, Serialize};
use crate::icon_descriptor::IconDescriptor;
use crate::rgb::RGB8WithOptionalA;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyStyle {
pub label: Option<String>,
pub icon: Option<IconDescriptor>,
pub border: Option<RGB8WithOptionalA>,
}
impl KeyStyle {
pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle {
KeyStyle {
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
border: self.border.or(base.border),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobStyle {
pub label: Option<String>,
pub icon: Option<IconDescriptor>,
pub indicators: Option<KnobIndicators>,
}
impl KnobStyle {
pub fn merge_over(&self, base: &Self) -> Self {
Self {
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
indicators: self
.indicators
.as_ref()
.zip(base.indicators.as_ref())
.map(|(a, b)| a.merge_over(b))
.or_else(|| self.indicators.clone())
.or_else(|| base.indicators.clone()),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobIndicators {
pub bar: Option<KnobIndicatorBarConfig>,
pub circle: Option<KnobIndicatorCircleConfig>,
}
impl KnobIndicators {
pub fn merge_over(&self, base: &Self) -> Self {
Self {
bar: self
.bar
.as_ref()
.zip(base.bar.as_ref())
.map(|(a, b)| a.merge_over(b))
.or_else(|| self.bar.clone())
.or_else(|| base.bar.clone()),
circle: self
.circle
.as_ref()
.zip(base.circle.as_ref())
.map(|(a, b)| a.merge_over(b))
.or_else(|| self.circle.clone())
.or_else(|| base.circle.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobIndicatorBarConfig {
pub color: Option<RGB8WithOptionalA>,
}
impl KnobIndicatorBarConfig {
pub fn merge_over(&self, base: &Self) -> Self {
Self {
color: self.color.as_ref().or(base.color.as_ref()).cloned(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobIndicatorCircleConfig {
pub color: Option<RGB8WithOptionalA>,
pub width: Option<u8>,
pub radius: Option<u8>,
}
impl KnobIndicatorCircleConfig {
pub fn merge_over(&self, base: &Self) -> Self {
Self {
color: self.color.as_ref().or(base.color.as_ref()).cloned(),
width: self.width.as_ref().or(base.width.as_ref()).cloned(),
radius: self.radius.as_ref().or(base.radius.as_ref()).cloned(),
}
}
}

View file

@ -0,0 +1,14 @@
[package]
name = "loupedeck_serial"
version = "0.1.0"
edition = "2021"
[dependencies]
serialport = "4.3.0"
enum-ordinalize = "4.3.0"
enumset = "1.1.3"
bytes = "1.5.0"
thiserror = "1.0.52"
rgb = "0.8.37"
flume = "0.11.0"
serde = { version = "1.0.195", features = ["derive"] }

View file

@ -0,0 +1,271 @@
use enum_ordinalize::Ordinalize;
use enumset::{enum_set, EnumSet, EnumSetType};
use crate::util::Endianness;
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u8)]
pub enum LoupedeckKnob {
LeftTop = 0x01,
LeftMiddle = 0x02,
LeftBottom = 0x03,
RightTop = 0x04,
RightMiddle = 0x05,
RightBottom = 0x06,
}
impl LoupedeckKnob {
fn is_left(&self) -> bool {
matches!(self, LoupedeckKnob::LeftTop | LoupedeckKnob::LeftMiddle | LoupedeckKnob::LeftBottom)
}
fn row(&self) -> u8 {
match self {
LoupedeckKnob::LeftTop | LoupedeckKnob::RightTop => 0,
LoupedeckKnob::LeftMiddle | LoupedeckKnob::RightMiddle => 1,
LoupedeckKnob::LeftBottom | LoupedeckKnob::RightBottom => 2,
}
}
}
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u8)]
pub enum LoupedeckButton {
N0 = 0x07,
N1 = 0x08,
N2 = 0x09,
N3 = 0x0a,
N4 = 0x0b,
N5 = 0x0c,
N6 = 0x0d,
N7 = 0x0e,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceDisplayConfiguration {
pub id: u8,
pub dpi: f32,
pub width: u16,
pub height: u16,
pub local_offset_x: u16,
pub local_offset_y: u16,
pub global_offset_x: u16,
pub global_offset_y: u16,
pub endianness: Endianness,
}
impl Eq for LoupedeckDeviceDisplayConfiguration {
fn assert_receiver_is_total_eq(&self) {}
}
impl PartialEq for LoupedeckDeviceDisplayConfiguration {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceKeyGridCharacteristics {
pub rows: u8,
pub columns: u8,
pub display: LoupedeckDeviceDisplayConfiguration,
}
#[derive(Debug)]
pub struct LoupedeckDisplayRect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl LoupedeckDeviceKeyGridCharacteristics {
pub fn key_size(&self) -> (u16, u16) {
// Assuming the sizes are integers
(self.display.width / self.columns as u16, self.display.height / self.rows as u16)
}
pub fn get_key_at_local_coordinates(&self, x: u16, y: u16) -> Option<u8> {
let (column_width, row_height) = self.key_size();
if x >= self.display.width || y >= self.display.height {
return None;
}
let column = (x / column_width) as u8;
let row = (y / row_height) as u8;
Some(row * self.columns + column)
}
pub fn get_key_at_global_coordinates(&self, x: u16, y: u16) -> Option<u8> {
if x < self.display.global_offset_x || y < self.display.global_offset_y {
return None;
}
let local_x = x - self.display.global_offset_x;
let local_y = y - self.display.global_offset_y;
self.get_key_at_local_coordinates(local_x, local_y)
}
pub fn get_local_key_rect(&self, key_index: u8) -> Option<LoupedeckDisplayRect> {
if key_index >= self.rows * self.columns {
return None;
}
let (column_width, row_height) = self.key_size();
let row = (key_index / self.columns) as u16;
let column = (key_index % self.columns) as u16;
Some(LoupedeckDisplayRect {
x: column * column_width,
y: row * row_height,
w: column_width,
h: row_height,
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceCharacteristics {
pub vendor_id: u16,
pub product_id: u16,
pub name: &'static str,
pub available_knobs: EnumSet<LoupedeckKnob>,
pub available_buttons: EnumSet<LoupedeckButton>,
pub key_grid: LoupedeckDeviceKeyGridCharacteristics,
pub knob_displays: Option<(LoupedeckDeviceDisplayConfiguration, LoupedeckDeviceDisplayConfiguration)>,
}
impl Eq for LoupedeckDeviceCharacteristics {
fn assert_receiver_is_total_eq(&self) {}
}
impl PartialEq for LoupedeckDeviceCharacteristics {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
impl LoupedeckDeviceCharacteristics {
pub fn knob_rows(&self) -> u8 {
self.available_knobs.iter().map(|k| k.row()).max().map(|v| v + 1).unwrap_or(0)
}
pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> {
let check = |display: &LoupedeckDeviceDisplayConfiguration| {
x >= display.global_offset_x
&& 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) {
Some(&self.key_grid.display)
} else if let Some(knob_displays) = &self.knob_displays {
if check(&knob_displays.0) {
Some(&knob_displays.0)
} else if check(&knob_displays.1) {
Some(&knob_displays.1)
} else {
None
}
} else {
None
}
}
pub fn get_display_and_rect_for_knob(&self, knob: LoupedeckKnob) -> Option<(&LoupedeckDeviceDisplayConfiguration, LoupedeckDisplayRect)> {
if !self.available_knobs.contains(knob) {
return None;
}
if let Some(knob_displays) = &self.knob_displays {
let display = if knob.is_left() { &knob_displays.0 } else { &knob_displays.1 };
let row = knob.row() as u16;
let rows = self.knob_rows() as u16;
let row_height = display.height / rows;
let rect = LoupedeckDisplayRect {
x: 0,
y: row_height * row,
w: display.width,
h: row_height,
};
Some((display, rect))
} else {
None
}
}
}
static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics {
vendor_id: 0x2ec2,
product_id: 0x0004,
name: "Loupedeck Live",
available_knobs: enum_set!(
LoupedeckKnob::LeftTop
| LoupedeckKnob::LeftMiddle
| LoupedeckKnob::LeftBottom
| LoupedeckKnob::RightTop
| LoupedeckKnob::RightMiddle
| LoupedeckKnob::RightBottom
),
available_buttons: enum_set!(
LoupedeckButton::N0
| LoupedeckButton::N1
| LoupedeckButton::N2
| LoupedeckButton::N3
| LoupedeckButton::N4
| LoupedeckButton::N5
| LoupedeckButton::N6
| LoupedeckButton::N7
),
key_grid: LoupedeckDeviceKeyGridCharacteristics {
rows: 3,
columns: 4,
display: LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 360,
height: 270,
local_offset_x: 60,
local_offset_y: 0,
global_offset_x: 60,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
},
knob_displays: Some((
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 60,
height: 270,
local_offset_x: 0,
local_offset_y: 0,
global_offset_x: 0,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 60,
height: 270,
local_offset_x: 420,
local_offset_y: 0,
global_offset_x: 420,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
)),
};
pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [&LOUPEDECK_LIVE_CHARACTERISTIC];

View file

@ -0,0 +1,65 @@
use bytes::Bytes;
use enum_ordinalize::Ordinalize;
use rgb::RGB8;
use serde::{Deserialize, Serialize};
use crate::characteristics::LoupedeckButton;
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Ordinalize)]
#[repr(u8)]
#[serde(rename_all = "kebab-case")]
pub enum VibrationPattern {
Short = 0x01,
Medium = 0x0a,
Long = 0x0f,
Low = 0x31,
ShortLow = 0x32,
ShortLower = 0x33,
Lower = 0x40,
Lowest = 0x41,
DescendSlow = 0x46,
DescendMed = 0x47,
DescendFast = 0x48,
AscendSlow = 0x52,
AscendMed = 0x53,
AscendFast = 0x58,
RevSlowest = 0x5e,
RevSlow = 0x5f,
RevMed = 0x60,
RevFast = 0x61,
RevFaster = 0x62,
RevFastest = 0x63,
RiseFall = 0x6a,
Buzz = 0x70,
Rumble5 = 0x77,
Rumble4 = 0x78,
Rumble3 = 0x79,
Rumble2 = 0x7a,
Rumble1 = 0x7b,
VeryLong = 0x76,
}
#[derive(Debug)]
pub(crate) enum LoupedeckCommand {
RequestSerialNumber,
RequestFirmwareVersion,
SetBrightness(f32),
SetButtonColor {
button: LoupedeckButton,
color: RGB8,
},
ReplaceFramebufferArea {
display_id: u8,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: Bytes,
},
RefreshDisplay {
display_id: u8,
},
Vibrate {
pattern: VibrationPattern,
},
}

View file

@ -0,0 +1,345 @@
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,
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
}
impl AvailableLoupedeckDevice {
pub fn port_name(&self) -> &str {
&self.port_name
}
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
self.characteristics
}
pub fn connect(&self) -> Result<LoupedeckDevice, ConnectError> {
LoupedeckDevice::connect(self)
}
}
#[derive(Debug)]
pub struct LoupedeckDevice {
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
pub(crate) serial_number: String,
pub(crate) firmware_version: String,
events_receiver: flume::Receiver<LoupedeckEvent>,
commands_sender: flume::Sender<LoupedeckCommand>,
}
#[derive(Debug, Error)]
pub enum ConnectError {
#[error("Serial port error: {0}")]
SerialPort(#[from] serialport::Error),
#[error("IO error: {0}")]
IO(#[from] io::Error),
#[error("The device did not respond with the expected handshake response (early-stage).")]
WrongEarlyHandshakeResponse,
#[error("The device did not respond with the expected handshake response (late-stage).")]
WrongLateHandshakeResponse,
#[error("The device was already connected.")]
AlreadyConnected,
}
#[derive(Debug, Error)]
pub enum RefreshDisplayError {
#[error("The specified display is not available for this device.")]
UnknownDisplay,
}
#[derive(Debug, Error)]
pub enum SetButtonColorError {
#[error("The specified button is not available for this device.")]
UnknownButton,
}
#[derive(Debug, Error)]
pub enum ReplaceFramebufferAreaError {
#[error("The specified display is not available for this device.")]
UnknownDisplay,
#[error("The area is not (fully) within the bounds of the display.")]
OutOfBounds,
#[error("Given the specified dimensions, the buffer size must be {expected} but it was {actual}.")]
WrongBufferSize { expected: usize, actual: usize },
}
impl LoupedeckDevice {
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
self.characteristics
}
pub fn serial_number(&self) -> &String {
&self.serial_number
}
pub fn firmware_version(&self) -> &String {
&self.firmware_version
}
pub fn events(&self) -> flume::Receiver<LoupedeckEvent> {
self.events_receiver.clone()
}
pub fn set_brightness(&self, value: f32) {
self.commands_sender.send(LoupedeckCommand::SetBrightness(value)).unwrap();
}
pub fn set_button_color(&self, button: LoupedeckButton, color: RGB8) -> Result<(), SetButtonColorError> {
if !self.characteristics.available_buttons.contains(button) {
return Err(SetButtonColorError::UnknownButton);
}
// 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();
Ok(())
}
/// Replaces the specified framebuffer area of the display with `buffer`.
///
/// `buffer` must contain exactly as many pixels as required.
///
/// Please note that the _internal_ color format of all currently known devices is RGB565 (16 bits in total).
pub fn replace_framebuffer_area(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: &[RGB8],
) -> Result<(), ReplaceFramebufferAreaError> {
self.check_replace_framebuffer_area_parameters(display, x, y, width, height)?;
let expected_buffer_size = (height * width) as usize;
if buffer.len() != expected_buffer_size {
return Err(ReplaceFramebufferAreaError::WrongBufferSize {
expected: expected_buffer_size,
actual: buffer.len(),
});
}
if width == 0 || height == 0 {
return Ok(());
}
// For some color values x, the pixel brightness is lower than for x - 1.
// I dont understand why and what is the pattern of these values.
let converted_buffer = convert_rgb888_to_rgb565(Bytes::copy_from_slice(buffer.as_slice()), display.endianness);
self.unchecked_replace_framebuffer_area_raw(display, x, y, width, height, converted_buffer.freeze());
Ok(())
}
/// Replaces the specified framebuffer area of the display with `buffer`.
///
/// `buffer` must contain exactly (`width * height`) RGB565 (16 bit) values, LE or BE depending on `display.endianness`.
///
/// If you have a buffer of `rgb::RGB8` values, you should use [replace_framebuffer_area](LoupedeckDevice::replace_framebuffer_area) instead.
pub fn replace_framebuffer_area_raw(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: Bytes,
) -> Result<(), ReplaceFramebufferAreaError> {
self.check_replace_framebuffer_area_parameters(display, x, y, width, height)?;
let expected_buffer_size = (height * width * 2) as usize;
if buffer.len() != expected_buffer_size {
return Err(ReplaceFramebufferAreaError::WrongBufferSize {
expected: expected_buffer_size,
actual: buffer.len(),
});
}
if width == 0 || height == 0 {
return Ok(());
}
self.unchecked_replace_framebuffer_area_raw(display, x, y, width, height, buffer);
Ok(())
}
fn check_replace_framebuffer_area_parameters(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
) -> Result<(), ReplaceFramebufferAreaError> {
if !(display.id == self.characteristics.key_grid.display.id
|| self
.characteristics
.knob_displays
.as_ref()
.is_some_and(|d| d.0.id == display.id || d.1.id == display.id))
{
return Err(ReplaceFramebufferAreaError::UnknownDisplay);
}
if x + width > display.width || y + height > display.height {
return Err(ReplaceFramebufferAreaError::OutOfBounds);
}
Ok(())
}
fn unchecked_replace_framebuffer_area_raw(&self, display: &LoupedeckDeviceDisplayConfiguration, x: u16, y: u16, width: u16, height: u16, buffer: Bytes) {
let local_x = display.local_offset_x + x;
let local_y = display.local_offset_y + y;
self.commands_sender
.send(LoupedeckCommand::ReplaceFramebufferArea {
display_id: display.id,
x: local_x,
y: local_y,
width,
height,
buffer,
})
.unwrap();
}
pub fn refresh_display(&self, display: &LoupedeckDeviceDisplayConfiguration) -> Result<(), RefreshDisplayError> {
if !(display.id == self.characteristics.key_grid.display.id
|| self
.characteristics
.knob_displays
.as_ref()
.is_some_and(|d| d.0.id == display.id || d.1.id == display.id))
{
return Err(RefreshDisplayError::UnknownDisplay);
}
self.commands_sender.send(LoupedeckCommand::RefreshDisplay { display_id: display.id }).unwrap();
Ok(())
}
pub fn vibrate(&self, pattern: VibrationPattern) {
self.commands_sender.send(LoupedeckCommand::Vibrate { pattern }).unwrap();
}
pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> {
let ports = serialport::available_ports()?;
Ok(ports
.iter()
.filter_map(|port| {
if let SerialPortType::UsbPort(info) = &port.port_type {
let characteristics = CHARACTERISTICS.iter().find(|c| c.vendor_id == info.vid && c.product_id == info.pid);
if let Some(characteristics) = characteristics {
return Some(AvailableLoupedeckDevice {
port_name: port.port_name.clone(),
characteristics,
});
}
}
None
})
.collect::<Vec<_>>())
}
pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result<LoupedeckDevice, ConnectError> {
let mut port = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?;
let mut buf = [0; WS_UPGRADE_RESPONSE_START.len()];
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, 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) = 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) = 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(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(10)) {
Ok(LoupedeckInternalEvent::GetFirmwareVersionResponse { firmware_version }) => Ok(firmware_version),
_ => Err(ConnectError::WrongLateHandshakeResponse),
}?;
drop(internal_events_receiver);
Ok(LoupedeckDevice {
characteristics,
serial_number,
firmware_version,
events_receiver: public_events_receiver,
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

@ -0,0 +1,24 @@
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RotationDirection {
Clockwise,
Counterclockwise,
}
#[derive(Debug)]
pub(crate) enum LoupedeckInternalEvent {
GetSerialNumberResponse { serial_number: String },
GetFirmwareVersionResponse { firmware_version: String },
}
#[derive(Debug)]
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

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

View file

@ -0,0 +1,303 @@
use std::cmp::min;
use std::io;
use std::io::{ErrorKind, Read, Write};
use std::sync::mpsc;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use enum_ordinalize::Ordinalize;
use serialport::SerialPort;
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
use crate::commands::LoupedeckCommand;
use crate::events::RotationDirection::{Clockwise, Counterclockwise};
use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html
HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: 123abc
"#;
pub(crate) const WS_UPGRADE_RESPONSE_START: &str = "HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s=";
const MESSAGE_START_BYTE: u8 = 0x82;
const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize;
enum ParseMessageResult {
InternalEvent(LoupedeckInternalEvent),
PublicEvent(LoupedeckEvent),
Nothing,
}
impl From<LoupedeckInternalEvent> for ParseMessageResult {
fn from(value: LoupedeckInternalEvent) -> Self {
ParseMessageResult::InternalEvent(value)
}
}
impl From<LoupedeckEvent> for ParseMessageResult {
fn from(value: LoupedeckEvent) -> Self {
ParseMessageResult::PublicEvent(value)
}
}
pub(crate) fn read_messages_worker(
mut port: Box<dyn SerialPort>,
public_sender: flume::Sender<LoupedeckEvent>,
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
) {
let mut internal_sender = Some(internal_sender);
let mut should_stop = false;
let mut buffer = BytesMut::new();
while !should_stop {
let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH);
let read_result = port.read(&mut chunk);
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);
while !should_stop {
let start_index = buffer.iter().position(|b| *b == MESSAGE_START_BYTE);
if let Some(start_index) = start_index {
if start_index > 0 {
buffer.advance(start_index);
if buffer.remaining() == 0 {
break;
}
}
let length = buffer[1] as usize + 2;
if length > buffer.remaining() {
break;
} else {
let mut message = buffer.split_to(length);
let command = message[3];
// let transaction_id = message[4];
message.advance(5);
let result = parse_message(command, message.freeze());
match result {
ParseMessageResult::InternalEvent(event) => {
// Does nothing after the receiving side has been closed
if let Some(sender) = internal_sender.take() {
let is_open = sender.send(event).is_ok();
if is_open {
internal_sender = Some(sender);
}
}
}
ParseMessageResult::PublicEvent(event) => {
if public_sender.send(event).is_err() {
should_stop = false
}
}
ParseMessageResult::Nothing => {}
}
}
} else {
break;
}
}
}
}
fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
match command {
0x00 => match message[1] {
0x00 => match message[0] {
0x01 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::LeftTop },
0x02 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::LeftMiddle,
},
0x03 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::LeftBottom,
},
0x04 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::RightTop },
0x05 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::RightMiddle,
},
0x06 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::RightBottom,
},
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::LeftTop },
0x02 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::LeftMiddle,
},
0x03 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::LeftBottom,
},
0x04 => LoupedeckEvent::KnobUp { knob: LoupedeckKnob::RightTop },
0x05 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::RightMiddle,
},
0x06 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::RightBottom,
},
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 => {
let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID");
LoupedeckEvent::KnobRotate {
knob,
direction: if message[1] == 1 { Clockwise } else { Counterclockwise },
}
.into()
}
0x03 => LoupedeckInternalEvent::GetSerialNumberResponse {
serial_number: String::from_utf8_lossy(&message).into_owned(),
}
.into(),
0x07 => LoupedeckInternalEvent::GetFirmwareVersionResponse {
firmware_version: format!("{}.{}.{}", message[0], message[1], message[2]),
}
.into(),
0x4d | 0x6d => {
message.advance(1);
let x = message.get_u16();
let y = message.get_u16();
let touch_id = message.get_u8();
LoupedeckEvent::Touch {
touch_id,
x,
y,
is_end: command == 0x6d,
}
.into()
}
_ => ParseMessageResult::Nothing,
}
}
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> {
if next_transaction_id == 0 {
next_transaction_id += 1;
}
let mut data_with_header = BytesMut::with_capacity(data.len() + 3);
data_with_header.put_u8(min(u8::MAX as usize, data.len() + 3) as u8);
data_with_header.put_u8(command_id);
data_with_header.put_u8(next_transaction_id);
data_with_header.put(data);
let length = data_with_header.len();
if length > u8::MAX as usize {
let mut prep = BytesMut::with_capacity(14);
prep.put_u8(0x82);
prep.put_u8(0xff);
prep.put_bytes(0x00, 4);
prep.put_u32(length as u32);
prep.put_bytes(0x00, 4);
port.write_all(&prep)?;
port.flush()?;
} else {
let mut prep = BytesMut::zeroed(6);
prep[0] = 0x82;
prep[1] = (0x80 + length) as u8;
port.write_all(&prep)?;
port.flush()?;
}
port.write_all(&data_with_header)?;
port.flush()?;
next_transaction_id = next_transaction_id.wrapping_add(1);
Ok(())
};
for command in receiver {
let result = match command {
LoupedeckCommand::RequestSerialNumber => send(0x03, Bytes::new()),
LoupedeckCommand::RequestFirmwareVersion => send(0x07, Bytes::new()),
LoupedeckCommand::SetBrightness(value) => {
let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8;
send(0x09, Bytes::copy_from_slice(&[raw_value]))
}
LoupedeckCommand::SetButtonColor { button, color } => send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b])),
LoupedeckCommand::ReplaceFramebufferArea {
display_id,
x,
y,
width,
height,
buffer,
} => {
let mut data = BytesMut::with_capacity(10 + buffer.len());
data.put_u8(0);
data.put_u8(display_id);
data.put_u16(x);
data.put_u16(y);
data.put_u16(width);
data.put_u16(height);
data.put(buffer);
send(0x10, data.freeze())
}
LoupedeckCommand::RefreshDisplay { display_id } => send(0x0f, Bytes::copy_from_slice(&[0, display_id])),
LoupedeckCommand::Vibrate { pattern } => send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])),
};
if let Err(error) = result {
match error.kind() {
ErrorKind::TimedOut | ErrorKind::BrokenPipe => break,
_ => {
panic!("IO error during write: {}", error);
}
}
}
}
}

View file

@ -0,0 +1,32 @@
use bytes::{BufMut, Bytes, BytesMut};
pub fn convert_rgb888_to_rgb565(original: Bytes, endianness: Endianness) -> BytesMut {
let pixel_count = original.len() / 3;
let excess_bytes = original.len() % 3;
if excess_bytes != 0 {
panic!("Found {} excess bytes at the end of the original buffer", excess_bytes);
}
let mut result = BytesMut::with_capacity(pixel_count * 2);
for index in 0..pixel_count {
let red = original[index * 3] as u16;
let green = original[index * 3 + 1] as u16;
let blue = original[index * 3 + 2] as u16;
let color = ((red & 0b11111000) << 8) | ((green & 0b11111100) << 3) | (blue.wrapping_shr(3));
match endianness {
Endianness::LittleEndian => result.put_u16_le(color),
Endianness::BigEndian => result.put_u16(color),
}
}
result
}
#[derive(Debug, Copy, Clone)]
pub enum Endianness {
BigEndian,
LittleEndian,
}

View file

@ -0,0 +1,10 @@
[package]
name = "pa_volume_interface"
version = "0.1.0"
edition = "2021"
[dependencies]
flume = "0.11.0"
im = "15.1.0"
tokio = { version = "1.35.1", default-features = false, features = ["sync"] }
libpulse-binding = "2.28.1"

View file

@ -0,0 +1,525 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Instant;
use im::HashMap;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, SinkInfo, SinkInputInfo, SourceInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
use libpulse_binding::context::{subscribe, Context, FlagSet};
use libpulse_binding::def::Retval;
use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
use libpulse_binding::operation::Operation;
use libpulse_binding::volume::{ChannelVolumes, Volume};
use tokio::sync::broadcast;
use tokio::sync::broadcast::Receiver;
pub type PaEntityId = u32;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum PaEntityKind {
Source,
Sink,
SinkInput,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum PaEntityMetadata {
Source {
name: String,
description: String,
},
Sink {
name: String,
description: String,
},
SinkInput {
description: String,
binary_name: Option<String>,
application_name: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct PaEntityState {
id: PaEntityId,
channel_volumes: ChannelVolumes,
is_muted: bool,
metadata: PaEntityMetadata,
}
impl PaEntityState {
pub fn id(&self) -> &PaEntityId {
&self.id
}
pub fn kind(&self) -> PaEntityKind {
match &self.metadata {
PaEntityMetadata::Source { .. } => PaEntityKind::Source,
PaEntityMetadata::Sink { .. } => PaEntityKind::Sink,
PaEntityMetadata::SinkInput { .. } => PaEntityKind::SinkInput,
}
}
pub fn metadata(&self) -> &PaEntityMetadata {
&self.metadata
}
pub fn channel_volumes(&self) -> Vec<f32> {
self.channel_volumes.get().iter().map(|v| v.0 as f32 / Volume::NORMAL.0 as f32).collect()
}
pub fn is_muted(&self) -> bool {
self.is_muted
}
}
impl From<&SourceInfo<'_>> for PaEntityState {
fn from(value: &SourceInfo) -> Self {
PaEntityState {
id: value.index as PaEntityId,
is_muted: value.mute,
channel_volumes: value.volume,
metadata: PaEntityMetadata::Source {
name: value.name.clone().unwrap_or_default().into_owned(),
description: value.description.clone().unwrap_or_default().into_owned(),
},
}
}
}
impl From<&SinkInfo<'_>> for PaEntityState {
fn from(value: &SinkInfo) -> Self {
PaEntityState {
id: value.index as PaEntityId,
is_muted: value.mute,
channel_volumes: value.volume,
metadata: PaEntityMetadata::Sink {
name: value.name.clone().unwrap_or_default().into_owned(),
description: value.description.clone().unwrap_or_default().into_owned(),
},
}
}
}
impl From<&SinkInputInfo<'_>> for PaEntityState {
fn from(value: &SinkInputInfo) -> Self {
PaEntityState {
id: value.index as PaEntityId,
is_muted: value.mute,
channel_volumes: value.volume,
metadata: PaEntityMetadata::SinkInput {
description: value.name.clone().unwrap_or_default().into_owned(),
application_name: value
.proplist
.get("application.name")
.map(|v| String::from_utf8_lossy(v).trim_end_matches(char::from(0)).to_owned()),
binary_name: value
.proplist
.get("application.process.binary")
.map(|v| String::from_utf8_lossy(v).trim_end_matches(char::from(0)).to_owned()),
},
}
}
}
#[derive(Debug, Clone)]
pub struct PaVolumeState {
timestamp: Instant,
entities_by_id: HashMap<PaEntityId, Arc<PaEntityState>>,
default_sink_id: PaEntityId,
default_source_id: PaEntityId,
}
impl PaVolumeState {
pub fn timestamp(&self) -> &Instant {
&self.timestamp
}
pub fn entities_by_id(&self) -> &HashMap<PaEntityId, Arc<PaEntityState>> {
&self.entities_by_id
}
pub fn default_sink_id(&self) -> &PaEntityId {
&self.default_sink_id
}
pub fn default_source_id(&self) -> &PaEntityId {
&self.default_source_id
}
}
impl Default for PaVolumeState {
fn default() -> Self {
PaVolumeState {
timestamp: Instant::now(),
entities_by_id: HashMap::new(),
default_sink_id: 0,
default_source_id: 0,
}
}
}
#[derive(Debug)]
enum PaCommand {
SetIsMuted { id: PaEntityId, value: bool },
SetChannelVolumes { id: PaEntityId, channel_volumes: Box<[f32]> },
Terminate,
}
struct PaThread {
mainloop: Rc<RefCell<Mainloop>>,
context: Rc<RefCell<Context>>,
introspector: Rc<RefCell<Introspector>>,
commands_rx: flume::Receiver<PaCommand>,
state_tx: broadcast::Sender<Arc<PaVolumeState>>,
current_state: Arc<RwLock<Option<Arc<PaVolumeState>>>>,
}
impl PaThread {
fn spawn(
client_name: String,
commands_rx: flume::Receiver<PaCommand>,
state_tx: broadcast::Sender<Arc<PaVolumeState>>,
current_state: Arc<RwLock<Option<Arc<PaVolumeState>>>>,
) {
thread::spawn(move || {
let mainloop = Mainloop::new().unwrap();
let context = Context::new(&mainloop, &client_name).unwrap();
let introspector = context.introspect();
let mut t = PaThread {
mainloop: Rc::new(RefCell::new(mainloop)),
context: Rc::new(RefCell::new(context)),
introspector: Rc::new(RefCell::new(introspector)),
commands_rx,
state_tx,
current_state,
};
t.init();
t.run();
});
}
fn init(&mut self) {
self.context.borrow_mut().connect(None, FlagSet::NOFLAGS, None).unwrap();
self.wait_for(|s| s.context.borrow().get_state() == libpulse_binding::context::State::Ready);
let introspector = Rc::clone(&self.introspector);
{
let entities_by_id = Rc::new(RefCell::new(HashMap::<PaEntityId, Arc<PaEntityState>>::new()));
let entities_by_id_c = Rc::clone(&entities_by_id);
self.wait_for_operation(introspector.borrow().get_sink_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_info_list failed"),
ListResult::End => {}
ListResult::Item(sink) => {
entities_by_id_c.borrow_mut().insert(sink.index as PaEntityId, Arc::new(sink.into()));
}
}))
.unwrap();
let entities_by_id_c = Rc::clone(&entities_by_id);
self.wait_for_operation(introspector.borrow().get_source_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_source_info_list failed"),
ListResult::End => {}
ListResult::Item(source) => {
entities_by_id_c.borrow_mut().insert(source.index as PaEntityId, Arc::new(source.into()));
}
}))
.unwrap();
let entities_by_id_c = Rc::clone(&entities_by_id);
self.wait_for_operation(introspector.borrow().get_sink_input_info_list(move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_input_info_list failed"),
ListResult::End => {}
ListResult::Item(sink_input) => {
let id = sink_input.index as PaEntityId;
entities_by_id_c.borrow_mut().insert(id, Arc::new(sink_input.into()));
}
}))
.unwrap();
PaThread::set_state(
&self.current_state,
&self.state_tx,
Arc::new(PaVolumeState {
timestamp: Instant::now(),
entities_by_id: Rc::into_inner(entities_by_id).unwrap().into_inner(),
default_sink_id: 0,
default_source_id: 0,
}),
);
};
self.context
.borrow_mut()
.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE | InterestMaskSet::SINK_INPUT, |success| {
if !success {
panic!("Context.subscribe failed")
}
});
}
fn run(mut self) {
let introspector = Rc::clone(&self.introspector);
let current_state = Arc::clone(&self.current_state);
let state_tx = self.state_tx.clone();
self.context
.borrow_mut()
.set_subscribe_callback(Some(Box::new(move |facility, operation, entity_id| {
let entity_id = entity_id as PaEntityId;
let facility = facility.unwrap();
let timestamp = Instant::now();
let current_state = Arc::clone(&current_state);
match operation.unwrap() {
subscribe::Operation::Removed => {
let state = PaThread::unwrap_state(&current_state);
let entities_by_id = state.entities_by_id.without(&entity_id);
let default_source_id = if entity_id == state.default_source_id { 0 } else { state.default_source_id };
let default_sink_id = if entity_id == state.default_sink_id { 0 } else { state.default_sink_id };
PaThread::set_state(
&current_state,
&state_tx,
Arc::new(PaVolumeState {
timestamp,
entities_by_id,
default_source_id,
default_sink_id,
}),
);
}
subscribe::Operation::New | subscribe::Operation::Changed => {
let state_tx = state_tx.clone();
match facility {
Facility::SinkInput => {
introspector.borrow().get_sink_input_info(entity_id, move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_input_info failed"),
ListResult::End => {}
ListResult::Item(sink_input) => {
let state = PaThread::unwrap_state(&current_state);
let id = sink_input.index as PaEntityId;
let entities_by_id = state.entities_by_id.update(id, Arc::new(sink_input.into()));
PaThread::set_state(
&current_state,
&state_tx,
Arc::new(PaVolumeState {
timestamp,
entities_by_id,
..*state
}),
);
}
});
}
Facility::Sink => {
introspector.borrow().get_sink_info_by_index(entity_id, move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_sink_info_by_index failed"),
ListResult::End => {}
ListResult::Item(sink_input) => {
let state = PaThread::unwrap_state(&current_state);
let id = sink_input.index as PaEntityId;
let entities_by_id = state.entities_by_id.update(id, Arc::new(sink_input.into()));
PaThread::set_state(
&current_state,
&state_tx,
Arc::new(PaVolumeState {
timestamp,
entities_by_id,
..*state
}),
);
}
});
}
Facility::Source => {
introspector.borrow().get_source_info_by_index(entity_id, move |list_result| match list_result {
ListResult::Error => panic!("Introspector.get_source_info_by_index failed"),
ListResult::End => {}
ListResult::Item(sink_input) => {
let state = PaThread::unwrap_state(&current_state);
let id = sink_input.index as PaEntityId;
let entities_by_id = state.entities_by_id.update(id, Arc::new(sink_input.into()));
PaThread::set_state(
&current_state,
&state_tx,
Arc::new(PaVolumeState {
timestamp,
entities_by_id,
..*state
}),
);
}
});
}
_ => {}
};
}
};
})));
let current_state = Arc::clone(&self.current_state);
let mainloop = Rc::clone(&self.mainloop);
loop {
self.run_single_mainloop_iteration(false);
while let Ok(command) = self.commands_rx.try_recv() {
match command {
PaCommand::SetIsMuted { id, value } => {
if let Some(state) = PaThread::unwrap_state(&current_state).entities_by_id.get(&id) {
match state.kind() {
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_mute_by_index(id, value, None),
PaEntityKind::Source => self.introspector.borrow_mut().set_source_mute_by_index(id, value, None),
PaEntityKind::SinkInput => self.introspector.borrow_mut().set_sink_input_mute(id, value, None),
};
}
}
PaCommand::SetChannelVolumes { id, channel_volumes } => {
if let Some(state) = PaThread::unwrap_state(&current_state).entities_by_id.get(&id) {
let mut value = state.channel_volumes;
for (i, v) in channel_volumes.iter().enumerate() {
value.set(i as u8, Volume((Volume::NORMAL.0 as f32 * v).floor() as u32));
}
match state.kind() {
PaEntityKind::Sink => self.introspector.borrow_mut().set_sink_volume_by_index(id, &value, None),
PaEntityKind::Source => self.introspector.borrow_mut().set_source_volume_by_index(id, &value, None),
PaEntityKind::SinkInput => self.introspector.borrow_mut().set_sink_input_volume(id, &value, None),
};
}
}
PaCommand::Terminate => {
mainloop.borrow_mut().quit(Retval(0));
}
}
}
}
}
fn unwrap_state(state: &Arc<RwLock<Option<Arc<PaVolumeState>>>>) -> Arc<PaVolumeState> {
state
.read()
.unwrap()
.as_ref()
.cloned()
.expect("this function is only called after the initial state was set")
}
fn set_state(current_state: &RwLock<Option<Arc<PaVolumeState>>>, state_tx: &broadcast::Sender<Arc<PaVolumeState>>, value: Arc<PaVolumeState>) {
let mut s = current_state.write().unwrap();
*s = Some(Arc::clone(&value));
state_tx.send(value).unwrap();
}
fn run_single_mainloop_iteration(&mut self, block: bool) {
match self.mainloop.borrow_mut().iterate(block) {
IterateResult::Quit(_) => {
panic!("Mainloop quit.")
}
IterateResult::Err(e) => {
panic!("Mainloop error: {}", e)
}
IterateResult::Success(_) => {}
}
}
fn wait_for(&mut self, predicate: impl Fn(&mut Self) -> bool) {
loop {
self.run_single_mainloop_iteration(true);
if predicate(self) {
break;
}
}
}
fn wait_for_operation<T: ?Sized>(&mut self, operation: Operation<T>) -> Result<(), ()> {
loop {
self.run_single_mainloop_iteration(true);
match operation.get_state() {
libpulse_binding::operation::State::Running => {}
libpulse_binding::operation::State::Done => return Ok(()),
libpulse_binding::operation::State::Cancelled => return Err(()),
}
}
}
}
#[derive(Debug)]
struct PaWorker {
commands_tx: flume::Sender<PaCommand>,
}
impl Drop for PaWorker {
fn drop(&mut self) {
self.commands_tx.send(PaCommand::Terminate).ok();
}
}
#[derive(Debug, Clone)]
pub struct PaVolumeInterface {
#[allow(unused)]
worker: Arc<PaWorker>,
current_state: Arc<RwLock<Option<Arc<PaVolumeState>>>>,
state_tx: broadcast::Sender<Arc<PaVolumeState>>,
commands_tx: flume::Sender<PaCommand>,
}
impl PaVolumeInterface {
pub fn spawn_thread(client_name: String) -> PaVolumeInterface {
let (commands_tx, commands_rx) = flume::unbounded();
let state_tx = broadcast::Sender::new(5);
let current_state = Arc::new(RwLock::new(None));
PaThread::spawn(client_name, commands_rx, state_tx.clone(), Arc::clone(&current_state));
let worker = PaWorker {
commands_tx: commands_tx.clone(),
};
PaVolumeInterface {
worker: Arc::new(worker),
current_state,
commands_tx,
state_tx,
}
}
pub fn subscribe_to_state(&self) -> (Option<Arc<PaVolumeState>>, Receiver<Arc<PaVolumeState>>) {
let rx = self.state_tx.subscribe();
let state = self.current_state();
(state, rx)
}
pub fn current_state(&self) -> Option<Arc<PaVolumeState>> {
self.current_state.read().unwrap().clone()
}
pub fn set_is_muted(&self, id: PaEntityId, value: bool) {
self.commands_tx.send(PaCommand::SetIsMuted { id, value }).unwrap()
}
pub fn set_channel_volumes(&self, id: PaEntityId, channel_volumes: impl Into<Box<[f32]>>) {
self.commands_tx
.send(PaCommand::SetChannelVolumes {
id,
channel_volumes: channel_volumes.into(),
})
.unwrap()
}
}