diff --git a/.gitignore b/.gitignore index 40d9aca..0084c97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -/.idea \ No newline at end of file +/.idea +/examples/full/handlers \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7fac143..2f9fc41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,13 +1075,13 @@ dependencies = [ "color-eyre", "deckster_mode", "env_logger", - "im", "log", - "once_cell", "pa_volume_interface", "parse-display", + "regex", "serde", "serde_regex", + "tokio", ] [[package]] @@ -1236,9 +1236,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -1248,9 +1248,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", diff --git a/crates/deckster_mode/src/lib.rs b/crates/deckster_mode/src/lib.rs index 1edb992..47fcacb 100644 --- a/crates/deckster_mode/src/lib.rs +++ b/crates/deckster_mode/src/lib.rs @@ -6,10 +6,10 @@ 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}; +pub use deckster_shared as shared; +use deckster_shared::handler_communication::{ + HandlerCommand, HandlerEvent, HandlerInitializationError, HandlerInitializationResultMessage, InitialHandlerMessage, +}; #[derive(Debug, Error)] pub enum RunError { @@ -86,3 +86,7 @@ pub fn run< Ok(()) } + +pub fn send_command(command: HandlerCommand) { + println!("{}", serde_json::to_string(&command).unwrap()); +} diff --git a/crates/deckster_shared/src/rgb.rs b/crates/deckster_shared/src/rgb.rs index 6e6998d..9a587e8 100644 --- a/crates/deckster_shared/src/rgb.rs +++ b/crates/deckster_shared/src/rgb.rs @@ -24,7 +24,7 @@ fn parse_rgb8_from_hex_str(s: &str) -> Result { } 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)) + f.write_fmt(format_args!("#{:02x}{:02x}{:02x}", v.r, v.g, v.b)) } fn parse_rgba8_from_hex_str(s: &str) -> Result { @@ -42,7 +42,7 @@ fn parse_rgba8_from_hex_str(s: &str) -> Result { } 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)) + f.write_fmt(format_args!("#{:02x}{:02x}{:02x}{:02x}", v.r, v.g, v.b, v.a)) } fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result { diff --git a/crates/pa_volume_interface/src/lib.rs b/crates/pa_volume_interface/src/lib.rs index 3f83646..dd719b6 100644 --- a/crates/pa_volume_interface/src/lib.rs +++ b/crates/pa_volume_interface/src/lib.rs @@ -422,7 +422,9 @@ impl PaThread { fn set_state(current_state: &RwLock>>, state_tx: &broadcast::Sender>, value: Arc) { let mut s = current_state.write().unwrap(); *s = Some(Arc::clone(&value)); - state_tx.send(value).unwrap(); + + // If there are no subscribers, that’s ok. + _ = state_tx.send(value); } fn run_single_mainloop_iteration(&mut self, block: bool) { diff --git a/examples/full/handlers/pa_volume b/examples/full/handlers/pa_volume deleted file mode 100755 index 682cd79..0000000 Binary files a/examples/full/handlers/pa_volume and /dev/null differ diff --git a/handlers/pa_volume/Cargo.toml b/handlers/pa_volume/Cargo.toml index 35491cd..94cac05 100644 --- a/handlers/pa_volume/Cargo.toml +++ b/handlers/pa_volume/Cargo.toml @@ -10,8 +10,8 @@ clap = { version = "4.4.18", features = ["derive"] } color-eyre = "0.6.2" serde = { version = "1.0.196", features = ["derive"] } serde_regex = "1.1.0" +regex = "1.10.3" parse-display = "0.8.2" -once_cell = "1.19.0" env_logger = "0.11.1" log = "0.4.20" -im = "15.1.0" \ No newline at end of file +tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt-multi-thread", "sync"] } \ No newline at end of file diff --git a/handlers/pa_volume/src/handler.rs b/handlers/pa_volume/src/handler.rs index dc6e12d..7f57c00 100644 --- a/handlers/pa_volume/src/handler.rs +++ b/handlers/pa_volume/src/handler.rs @@ -1,10 +1,18 @@ -use log::warn; -use once_cell::sync::Lazy; -use parse_display::helpers::regex::Regex; -use parse_display::Display; -use serde::Deserialize; +use std::sync::Arc; -use deckster_mode::{DecksterHandler, HandlerEvent, HandlerInitializationError, InitialHandlerMessage, KnobPath, KnobStyleByStateMap}; +use log::warn; +use parse_display::Display; +use regex::Regex; +use serde::Deserialize; +use tokio::select; +use tokio::sync::broadcast; + +use deckster_mode::shared::handler_communication::{ + HandlerCommand, HandlerEvent, HandlerInitializationError, InitialHandlerMessage, KnobEvent, RotationDirection, +}; +use deckster_mode::shared::path::KnobPath; +use deckster_mode::shared::state::KnobStyleByStateMap; +use deckster_mode::{send_command, DecksterHandler}; use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface}; #[derive(Debug, Clone, Deserialize)] @@ -92,8 +100,6 @@ pub enum State { Muted, } -static PA_VOLUME_INTERFACE: Lazy = Lazy::new(|| PaVolumeInterface::spawn_thread("deckster".to_owned())); - fn get_volume_from_cv(channel_volumes: &[f32]) -> f32 { *channel_volumes.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() } @@ -141,19 +147,156 @@ fn state_matches(target: &Target, state: &PaEntityState) -> bool { } pub struct Handler { - knobs: im::HashMap, + events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, + #[allow(unused)] + runtime: tokio::runtime::Runtime, } impl Handler { pub fn new(data: InitialHandlerMessage<(), KnobConfig>) -> Result { - Ok(Handler { - knobs: data.knob_configs.into_iter().map(|(p, (_, c))| (p, c)).collect(), - }) + let (events_sender, _) = broadcast::channel::<(KnobPath, KnobEvent)>(5); + let pa_volume_interface = Arc::new(PaVolumeInterface::spawn_thread("deckster handler".to_owned())); + + let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(1).build().unwrap(); + + for (path, (mode, config)) in data.knob_configs { + if !mode.is_empty() { + return Err(HandlerInitializationError::InvalidModeString { + message: "No mode string allowed.".into(), + }); + } + + let events_receiver = events_sender.subscribe(); + let a = Arc::clone(&pa_volume_interface); + runtime.spawn(manage_knob(path, config, events_receiver, a)); + } + + Ok(Handler { events_sender, runtime }) } } impl DecksterHandler for Handler { fn handle(&mut self, event: HandlerEvent) { - dbg!(&self.knobs, event); + if let HandlerEvent::Knob { path, event } = event { + self.events_sender.send((path, event)).unwrap(); + } + } +} + +async fn manage_knob(path: KnobPath, config: KnobConfig, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, pa_volume_interface: Arc) { + let mut entity_state: Option> = None; + + let (initial_state, mut volume_states) = pa_volume_interface.subscribe_to_state(); + + let update_knob_value = { + let config = &config; + let path = path.clone(); + + move |entity_state: &Option>| { + send_command(HandlerCommand::SetKnobValue { + path: path.clone(), + value: entity_state.as_ref().map(|s| { + if s.is_muted() && config.muted_turn_action == MutedTurnAction::UnmuteAtZero { + 0.0 + } else { + get_volume_from_cv(&s.channel_volumes()) + } + }), + }); + } + }; + + let update_knob_style = { + let config = &config; + let path = path.clone(); + + move |entity_state: &Option>| { + let state = match entity_state { + None => State::Inactive, + Some(s) if s.is_muted() => State::Muted, + Some(_) => State::Active, + }; + + let mut style = config.style.get(&state).cloned(); + + if let Some(ref mut s) = &mut style { + let v = entity_state.as_ref().map(|s| get_volume_from_cv(&s.channel_volumes())); + + if let Some(ref mut label) = &mut s.label { + if let Some(v) = v { + // v is only None when state is State::Inactive + *label = label.replace("{percentage}", &((v * 100.0).round() as u32).to_string()); + } + } + } + + send_command(HandlerCommand::SetKnobStyle { + path: path.clone(), + value: style, + }); + } + }; + + if let Some(state) = initial_state { + entity_state = state + .entities_by_id() + .values() + .find(|entity| state_matches(&config.target, entity)) + .map(Arc::clone); + } + + loop { + select! { + Ok(volume_state) = volume_states.recv() => { + entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone); + update_knob_style(&entity_state); + update_knob_value(&entity_state); + } + + Ok((event_path, event)) = events.recv() => { + if event_path != path { + continue; + } + + if let Some(entity_state) = &entity_state { + match event { + KnobEvent::Rotate { direction } => { + let factor: f32 = match direction { + RotationDirection::Clockwise => 1.0, + RotationDirection::Counterclockwise => -1.0, + }; + + let mut current_v = get_volume_from_cv(&entity_state.channel_volumes()); + + if entity_state.is_muted() { + match config.muted_turn_action { + MutedTurnAction::Ignore => continue, + MutedTurnAction::UnmuteAtZero => { + current_v = 0.0 + } + MutedTurnAction::Unmute => {}, + MutedTurnAction::Normal => {} + } + } + + let new_v = (current_v + (factor * config.delta.unwrap_or(0.01))).clamp(0.0, 1.0); + if new_v > 0.0 && matches!(config.muted_turn_action, MutedTurnAction::Unmute | MutedTurnAction::UnmuteAtZero) { + pa_volume_interface.set_is_muted(*entity_state.id(), false); + } + + pa_volume_interface.set_channel_volumes(*entity_state.id(), vec![new_v; entity_state.channel_volumes().len()]); + } + KnobEvent::Press => { + let is_muted = entity_state.is_muted(); + + if (is_muted && !config.disable_press_to_unmute) || (!is_muted && !config.disable_press_to_mute) { + pa_volume_interface.set_is_muted(*entity_state.id(), !is_muted) + } + } + _ => {} + } + } + } + } } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 78ca450..a2ad1cc 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -42,6 +42,13 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { let (commands_sender, commands_receiver) = flume::bounded::(5); let (events_sender, events_receiver) = flume::bounded::(5); + commands_sender + .send(HandlerCommand::SetActivePages { + knob_page_id: config.initial.knob_page.clone(), + key_page_id: config.initial.key_page.clone(), + }) + .unwrap(); + let key_configs = config .key_pages_by_id .iter() @@ -116,13 +123,6 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { device.set_brightness(0.1); device.vibrate(VibrationPattern::RiseFall); - commands_sender - .send(HandlerCommand::SetActivePages { - knob_page_id: config.initial.knob_page.clone(), - key_page_id: config.initial.key_page.clone(), - }) - .unwrap(); - let io_worker_context = IoWorkerContext::create(config_directory, Arc::clone(&config), device, commands_sender.clone(), events_sender); let io_worker_thread = thread::Builder::new()