commit
This commit is contained in:
parent
b5a7ab3c6b
commit
ebb0552a13
9 changed files with 186 additions and 36 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
/.idea
|
/.idea
|
||||||
|
/examples/full/handlers
|
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -1075,13 +1075,13 @@ dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"deckster_mode",
|
"deckster_mode",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"im",
|
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
|
||||||
"pa_volume_interface",
|
"pa_volume_interface",
|
||||||
"parse-display",
|
"parse-display",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_regex",
|
"serde_regex",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1236,9 +1236,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.2"
|
version = "1.10.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1248,9 +1248,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
|
@ -6,10 +6,10 @@ use either::Either;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use deckster_shared::handler_communication::HandlerInitializationResultMessage;
|
pub use deckster_shared as shared;
|
||||||
pub use deckster_shared::handler_communication::{HandlerEvent, HandlerInitializationError, InitialHandlerMessage};
|
use deckster_shared::handler_communication::{
|
||||||
pub use deckster_shared::path::*;
|
HandlerCommand, HandlerEvent, HandlerInitializationError, HandlerInitializationResultMessage, InitialHandlerMessage,
|
||||||
pub use deckster_shared::state::{KeyStyleByStateMap, KnobStyleByStateMap};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RunError {
|
pub enum RunError {
|
||||||
|
@ -86,3 +86,7 @@ pub fn run<
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_command(command: HandlerCommand) {
|
||||||
|
println!("{}", serde_json::to_string(&command).unwrap());
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, ParsingError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_rgb8_as_hex_string(v: &RGB8, f: &mut Formatter<'_>) -> std::fmt::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<RGBA8, ParsingError> {
|
fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ParsingError> {
|
||||||
|
@ -42,7 +42,7 @@ fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ParsingError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_rgba8_as_hex_string(v: &RGBA8, f: &mut Formatter<'_>) -> std::fmt::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<RGBA8, ParsingError> {
|
fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result<RGBA8, ParsingError> {
|
||||||
|
|
|
@ -422,7 +422,9 @@ impl PaThread {
|
||||||
fn set_state(current_state: &RwLock<Option<Arc<PaVolumeState>>>, state_tx: &broadcast::Sender<Arc<PaVolumeState>>, value: Arc<PaVolumeState>) {
|
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();
|
let mut s = current_state.write().unwrap();
|
||||||
*s = Some(Arc::clone(&value));
|
*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) {
|
fn run_single_mainloop_iteration(&mut self, block: bool) {
|
||||||
|
|
Binary file not shown.
|
@ -10,8 +10,8 @@ clap = { version = "4.4.18", features = ["derive"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
serde = { version = "1.0.196", features = ["derive"] }
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
serde_regex = "1.1.0"
|
serde_regex = "1.1.0"
|
||||||
|
regex = "1.10.3"
|
||||||
parse-display = "0.8.2"
|
parse-display = "0.8.2"
|
||||||
once_cell = "1.19.0"
|
|
||||||
env_logger = "0.11.1"
|
env_logger = "0.11.1"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
im = "15.1.0"
|
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt-multi-thread", "sync"] }
|
|
@ -1,10 +1,18 @@
|
||||||
use log::warn;
|
use std::sync::Arc;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use parse_display::helpers::regex::Regex;
|
|
||||||
use parse_display::Display;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
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};
|
use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -92,8 +100,6 @@ pub enum State {
|
||||||
Muted,
|
Muted,
|
||||||
}
|
}
|
||||||
|
|
||||||
static PA_VOLUME_INTERFACE: Lazy<PaVolumeInterface> = Lazy::new(|| PaVolumeInterface::spawn_thread("deckster".to_owned()));
|
|
||||||
|
|
||||||
fn get_volume_from_cv(channel_volumes: &[f32]) -> f32 {
|
fn get_volume_from_cv(channel_volumes: &[f32]) -> f32 {
|
||||||
*channel_volumes.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
|
*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 {
|
pub struct Handler {
|
||||||
knobs: im::HashMap<KnobPath, KnobConfig>,
|
events_sender: broadcast::Sender<(KnobPath, KnobEvent)>,
|
||||||
|
#[allow(unused)]
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
pub fn new(data: InitialHandlerMessage<(), KnobConfig>) -> Result<Self, HandlerInitializationError> {
|
pub fn new(data: InitialHandlerMessage<(), KnobConfig>) -> Result<Self, HandlerInitializationError> {
|
||||||
Ok(Handler {
|
let (events_sender, _) = broadcast::channel::<(KnobPath, KnobEvent)>(5);
|
||||||
knobs: data.knob_configs.into_iter().map(|(p, (_, c))| (p, c)).collect(),
|
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 {
|
impl DecksterHandler for Handler {
|
||||||
fn handle(&mut self, event: HandlerEvent) {
|
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<PaVolumeInterface>) {
|
||||||
|
let mut entity_state: Option<Arc<PaEntityState>> = 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<Arc<PaEntityState>>| {
|
||||||
|
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<Arc<PaEntityState>>| {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,13 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
|
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
|
||||||
let (events_sender, events_receiver) = flume::bounded::<HandlerEvent>(5);
|
let (events_sender, events_receiver) = flume::bounded::<HandlerEvent>(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
|
let key_configs = config
|
||||||
.key_pages_by_id
|
.key_pages_by_id
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -116,13 +123,6 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
device.set_brightness(0.1);
|
device.set_brightness(0.1);
|
||||||
device.vibrate(VibrationPattern::RiseFall);
|
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_context = IoWorkerContext::create(config_directory, Arc::clone(&config), device, commands_sender.clone(), events_sender);
|
||||||
|
|
||||||
let io_worker_thread = thread::Builder::new()
|
let io_worker_thread = thread::Builder::new()
|
||||||
|
|
Loading…
Add table
Reference in a new issue