commit
This commit is contained in:
parent
566ffb49f3
commit
13f307d387
12 changed files with 543 additions and 69 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -184,15 +184,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.31"
|
version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2004,6 +2004,20 @@ dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "timer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"color-eyre",
|
||||||
|
"deckster_mode",
|
||||||
|
"env_logger",
|
||||||
|
"humantime-serde",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
- Move loupedeck_serial and pa_volume_interface out of this repository.
|
- Move loupedeck_serial and pa_volume_interface out of this repository.
|
||||||
- Publish libraries to crates.io
|
- Publish libraries to crates.io
|
||||||
- Move handlers to their own repositories
|
- Move handlers to their own repositories
|
||||||
|
- Update dependencies
|
||||||
|
- Make the CLI of handlers more useful
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
### Terminology
|
### Terminology
|
||||||
|
|
|
@ -47,9 +47,43 @@ pub enum HandlerEvent {
|
||||||
Key { path: KeyPath, event: KeyEvent },
|
Key { path: KeyPath, event: KeyEvent },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Serialize, 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,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "command", rename_all = "kebab-case")]
|
#[serde(tag = "command", rename_all = "kebab-case")]
|
||||||
pub enum HandlerCommand {
|
pub enum HandlerCommand {
|
||||||
|
Vibrate { pattern: VibrationPattern },
|
||||||
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
|
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
|
||||||
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
|
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
|
||||||
SetKnobValue { path: KnobPath, value: Option<f32> },
|
SetKnobValue { path: KnobPath, value: Option<f32> },
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[keys.1x2]
|
[keys.1x2]
|
||||||
icon = "@ph/skip-back"
|
icon = "@ph/skip-back"
|
||||||
|
|
||||||
host = "remote"
|
|
||||||
handler = "playerctl"
|
handler = "playerctl"
|
||||||
config.mode = "previous"
|
config.mode = "previous"
|
||||||
config.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
|
config.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
|
||||||
|
@ -9,7 +8,6 @@ config.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
|
||||||
[keys.2x2]
|
[keys.2x2]
|
||||||
icon = "@ph/play-pause[alpha=0.4]"
|
icon = "@ph/play-pause[alpha=0.4]"
|
||||||
|
|
||||||
host = "remote"
|
|
||||||
handler = "playerctl"
|
handler = "playerctl"
|
||||||
config.mode = "play-pause"
|
config.mode = "play-pause"
|
||||||
config.style.paused.icon = "@ph/play"
|
config.style.paused.icon = "@ph/play"
|
||||||
|
@ -18,7 +16,6 @@ config.style.playing.icon = "@ph/pause"
|
||||||
[keys.3x2]
|
[keys.3x2]
|
||||||
icon = "@ph/skip-forward"
|
icon = "@ph/skip-forward"
|
||||||
|
|
||||||
host = "remote"
|
|
||||||
handler = "playerctl"
|
handler = "playerctl"
|
||||||
config.mode = "next"
|
config.mode = "next"
|
||||||
config.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
|
config.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
|
||||||
|
@ -26,7 +23,6 @@ config.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
|
||||||
[keys.1x3]
|
[keys.1x3]
|
||||||
icon = "@fad/shuffle[alpha=0.4]"
|
icon = "@fad/shuffle[alpha=0.4]"
|
||||||
|
|
||||||
host = "remote"
|
|
||||||
handler = "playerctl"
|
handler = "playerctl"
|
||||||
config.mode = "shuffle"
|
config.mode = "shuffle"
|
||||||
config.style.on.icon = "@fad/shuffle[color=#58fc11]"
|
config.style.on.icon = "@fad/shuffle[color=#58fc11]"
|
||||||
|
@ -34,20 +30,24 @@ config.style.on.icon = "@fad/shuffle[color=#58fc11]"
|
||||||
[keys.2x3]
|
[keys.2x3]
|
||||||
icon = "@fad/repeat[alpha=0.4]"
|
icon = "@fad/repeat[alpha=0.4]"
|
||||||
|
|
||||||
host = "remote"
|
|
||||||
handler = "playerctl"
|
handler = "playerctl"
|
||||||
config.mode = "loop"
|
config.mode = "loop"
|
||||||
config.style.single.icon = "@fad/repeat-one[color=#58fc11]"
|
config.style.single.icon = "@fad/repeat-one[color=#58fc11]"
|
||||||
config.style.all.icon = "@fad/repeat[color=#58fc11]"
|
config.style.all.icon = "@fad/repeat[color=#58fc11]"
|
||||||
|
|
||||||
[keys.3x3]
|
[keys.3x3]
|
||||||
icon = "@ph/timer[color=#ff0000]"
|
icon = "@ph/timer[scale=0.9]"
|
||||||
|
|
||||||
host = "moira"
|
|
||||||
handler = "timer"
|
handler = "timer"
|
||||||
config.durations = ["60s", "5m", "10m", "15m", "30m"]
|
config.durations = ["5s"]
|
||||||
|
config.select_timeout = "1500ms"
|
||||||
|
config.alarm_timeout = "10s"
|
||||||
config.vibrate_when_finished = true
|
config.vibrate_when_finished = true
|
||||||
config.needy = true
|
config.alarm_style_switch_interval = "200ms"
|
||||||
|
config.style.alarm1.border = "#ff0000ff"
|
||||||
|
config.style.alarm1.label = "00:00"
|
||||||
|
config.style.alarm2.border = "#ff00000f"
|
||||||
|
config.style.alarm2.label = "00:00"
|
||||||
|
|
||||||
[keys.4x3]
|
[keys.4x3]
|
||||||
icon = "@ph/computer-tower"
|
icon = "@ph/computer-tower"
|
||||||
|
|
14
handlers/timer/Cargo.toml
Normal file
14
handlers/timer/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "timer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
deckster_mode = { path = "../../crates/deckster_mode" }
|
||||||
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
|
color-eyre = "0.6.2"
|
||||||
|
env_logger = "0.11.1"
|
||||||
|
log = "0.4.20"
|
||||||
|
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "sync", "time"] }
|
||||||
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
|
humantime-serde = "1.1.1"
|
269
handlers/timer/src/handler.rs
Normal file
269
handlers/timer/src/handler.rs
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
use crate::util::{format_duration, get_far_future, spawn_debouncer};
|
||||||
|
use deckster_mode::shared::handler_communication::{
|
||||||
|
HandlerCommand, HandlerEvent, HandlerInitializationError, InitialHandlerMessage, KeyEvent, VibrationPattern,
|
||||||
|
};
|
||||||
|
use deckster_mode::shared::path::KeyPath;
|
||||||
|
use deckster_mode::shared::state::KeyStyleByStateMap;
|
||||||
|
use deckster_mode::{send_command, DecksterHandler};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
use tokio::sync::{broadcast, mpsc};
|
||||||
|
use tokio::task::LocalSet;
|
||||||
|
use tokio::time::{interval, interval_at, MissedTickBehavior};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct KeyConfig {
|
||||||
|
durations: Box<[humantime_serde::Serde<Duration>]>,
|
||||||
|
#[serde(with = "humantime_serde")]
|
||||||
|
select_timeout: Duration,
|
||||||
|
#[serde(with = "humantime_serde")]
|
||||||
|
alarm_timeout: Duration,
|
||||||
|
vibrate_when_finished: bool,
|
||||||
|
#[serde(with = "humantime_serde")]
|
||||||
|
alarm_style_switch_interval: Duration,
|
||||||
|
#[serde(default)]
|
||||||
|
style: KeyStyleByStateMap<KeyStyleState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
enum KeyStyleState {
|
||||||
|
Inactive,
|
||||||
|
Selection,
|
||||||
|
Running,
|
||||||
|
Paused,
|
||||||
|
Alarm1,
|
||||||
|
Alarm2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Handler {
|
||||||
|
events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
pub fn new(data: InitialHandlerMessage<KeyConfig, ()>) -> Result<Self, HandlerInitializationError> {
|
||||||
|
let events_sender = broadcast::Sender::<(KeyPath, KeyEvent)>::new(5);
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
let events_sender = events_sender.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread().enable_time().build().unwrap();
|
||||||
|
let task_set = LocalSet::new();
|
||||||
|
|
||||||
|
let (vibration_active_sender, mut vibration_active_receiver) = mpsc::channel::<bool>(2);
|
||||||
|
task_set.spawn_local(async move {
|
||||||
|
let mut active_count = 0usize;
|
||||||
|
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(2));
|
||||||
|
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
biased; // Important! Always first drain the channel before actually doing something.
|
||||||
|
|
||||||
|
result = vibration_active_receiver.recv() => {
|
||||||
|
match result {
|
||||||
|
None => break,
|
||||||
|
Some(false) => active_count -= 1,
|
||||||
|
Some(true) => {
|
||||||
|
active_count += 1;
|
||||||
|
|
||||||
|
if active_count == 1 {
|
||||||
|
interval.reset_immediately()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = interval.tick(), if active_count > 0 => {
|
||||||
|
send_command(HandlerCommand::Vibrate { pattern: VibrationPattern::Long })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (path, config) in data.key_configs {
|
||||||
|
task_set.spawn_local(manage_key(events_sender.subscribe(), vibration_active_sender.clone(), path, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.block_on(task_set)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Handler { events_sender })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecksterHandler for Handler {
|
||||||
|
fn handle(&mut self, event: HandlerEvent) {
|
||||||
|
if let HandlerEvent::Key { path, event } = event {
|
||||||
|
// No receivers being available can be ignored.
|
||||||
|
_ = self.events_sender.send((path, event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum KeyStage {
|
||||||
|
Inactive,
|
||||||
|
Selection { selected_index: usize },
|
||||||
|
Running { duration: Duration },
|
||||||
|
Paused { duration: Duration },
|
||||||
|
Alarm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct KeyState {
|
||||||
|
stage_change_time: Instant,
|
||||||
|
stage: KeyStage,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn manage_key(mut events: Receiver<(KeyPath, KeyEvent)>, vibration_active_sender: mpsc::Sender<bool>, path: KeyPath, config: KeyConfig) {
|
||||||
|
let mut state = KeyState {
|
||||||
|
stage_change_time: Instant::now(),
|
||||||
|
stage: KeyStage::Inactive,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (reset_timeout_sender, mut select_timeout_receiver) = spawn_debouncer(config.select_timeout);
|
||||||
|
|
||||||
|
let mut display_interval = interval(Duration::from_millis(500));
|
||||||
|
display_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
|
||||||
|
let mut alarm1_interval = interval_at(tokio::time::Instant::now() + Duration::ZERO, config.alarm_style_switch_interval * 2);
|
||||||
|
let mut alarm2_interval = interval_at(
|
||||||
|
tokio::time::Instant::now() + config.alarm_style_switch_interval,
|
||||||
|
config.alarm_style_switch_interval * 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
alarm1_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
alarm2_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
|
||||||
|
let mut alarm_sleep = Box::pin(tokio::time::sleep_until(get_far_future().into()));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut alarm_sleep => {
|
||||||
|
if state.stage == KeyStage::Alarm {
|
||||||
|
if config.vibrate_when_finished {
|
||||||
|
vibration_active_sender.send(false).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.stage = KeyStage::Inactive;
|
||||||
|
state.stage_change_time = Instant::now();
|
||||||
|
alarm_sleep.as_mut().reset(get_far_future().into());
|
||||||
|
send_key_style(&path, &state, &config);
|
||||||
|
} else {
|
||||||
|
if config.vibrate_when_finished {
|
||||||
|
vibration_active_sender.send(true).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.stage = KeyStage::Alarm;
|
||||||
|
state.stage_change_time = Instant::now();
|
||||||
|
alarm_sleep.as_mut().reset(tokio::time::Instant::now() + config.alarm_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = display_interval.tick(), if matches!(state.stage, KeyStage::Running { .. } | KeyStage::Paused { .. }) => {
|
||||||
|
send_key_style(&path, &state, &config);
|
||||||
|
}
|
||||||
|
_ = alarm1_interval.tick(), if matches!(state.stage, KeyStage::Alarm) => {
|
||||||
|
send_command(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&KeyStyleState::Alarm1).cloned() });
|
||||||
|
}
|
||||||
|
_ = alarm2_interval.tick(), if matches!(state.stage, KeyStage::Alarm) => {
|
||||||
|
send_command(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&KeyStyleState::Alarm2).cloned() });
|
||||||
|
}
|
||||||
|
_ = select_timeout_receiver.recv() => {
|
||||||
|
if let KeyStage::Selection { selected_index } = state.stage {
|
||||||
|
let duration = config.durations[selected_index].into_inner();
|
||||||
|
alarm_sleep.as_mut().reset(tokio::time::Instant::now() + duration);
|
||||||
|
state.stage = KeyStage::Running { duration };
|
||||||
|
state.stage_change_time = Instant::now();
|
||||||
|
send_key_style(&path, &state, &config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((p, event)) = events.recv() => {
|
||||||
|
if p != path {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == KeyEvent::Press {
|
||||||
|
match &mut state.stage {
|
||||||
|
KeyStage::Inactive => {
|
||||||
|
state.stage = KeyStage::Selection { selected_index: 0 };
|
||||||
|
_ = reset_timeout_sender.try_send(());
|
||||||
|
}
|
||||||
|
KeyStage::Selection { selected_index } => {
|
||||||
|
*selected_index = selected_index.wrapping_add(1) % config.durations.len();
|
||||||
|
_ = reset_timeout_sender.try_send(());
|
||||||
|
}
|
||||||
|
KeyStage::Running { duration } => {
|
||||||
|
state.stage = KeyStage::Paused { duration: *duration - state.stage_change_time.elapsed() };
|
||||||
|
}
|
||||||
|
KeyStage::Paused { duration } => {
|
||||||
|
alarm_sleep.as_mut().reset(tokio::time::Instant::now() + *duration);
|
||||||
|
state.stage = KeyStage::Running { duration: *duration };
|
||||||
|
}
|
||||||
|
KeyStage::Alarm => {
|
||||||
|
if config.vibrate_when_finished {
|
||||||
|
vibration_active_sender.send(false).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.stage = KeyStage::Inactive;
|
||||||
|
state.stage_change_time = Instant::now();
|
||||||
|
alarm_sleep.as_mut().reset(get_far_future().into());
|
||||||
|
send_command(HandlerCommand::SetKeyStyle { path: path.clone(), value: config.style.get(&KeyStyleState::Inactive).cloned() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.stage_change_time = Instant::now();
|
||||||
|
send_key_style(&path, &state, &config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_key_style(path: &KeyPath, state: &KeyState, config: &KeyConfig) {
|
||||||
|
let elapsed = state.stage_change_time.elapsed();
|
||||||
|
|
||||||
|
let style = match state.stage {
|
||||||
|
KeyStage::Inactive => config.style.get(&KeyStyleState::Inactive).cloned(),
|
||||||
|
KeyStage::Selection { selected_index } => {
|
||||||
|
let duration = config.durations[selected_index].into_inner();
|
||||||
|
let mut s = config.style.get(&KeyStyleState::Selection).cloned().unwrap_or_default();
|
||||||
|
s.label = Some(format_duration(duration));
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
KeyStage::Running { duration } => {
|
||||||
|
if elapsed > duration {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = config.style.get(&KeyStyleState::Running).cloned().unwrap_or_default();
|
||||||
|
s.label = Some(format_duration(duration - elapsed));
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
KeyStage::Paused { duration } => {
|
||||||
|
if elapsed > duration {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = config.style.get(&KeyStyleState::Paused).cloned().unwrap_or_default();
|
||||||
|
s.label = Some(if state.stage_change_time.elapsed().as_millis() % 1000 < 500 {
|
||||||
|
format_duration(duration - state.stage_change_time.elapsed())
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
KeyStage::Alarm => config.style.get(&KeyStyleState::Alarm1).cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
send_command(HandlerCommand::SetKeyStyle {
|
||||||
|
path: path.clone(),
|
||||||
|
value: style,
|
||||||
|
});
|
||||||
|
}
|
26
handlers/timer/src/main.rs
Normal file
26
handlers/timer/src/main.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use color_eyre::Result;
|
||||||
|
|
||||||
|
use crate::handler::Handler;
|
||||||
|
|
||||||
|
mod handler;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "timer")]
|
||||||
|
enum CliCommand {
|
||||||
|
#[command(name = "deckster-run", hide = true)]
|
||||||
|
Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let command = CliCommand::parse();
|
||||||
|
|
||||||
|
match command {
|
||||||
|
CliCommand::Run => {
|
||||||
|
deckster_mode::run(Handler::new)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
54
handlers/timer/src/util.rs
Normal file
54
handlers/timer/src/util.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::error::TrySendError;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
/// Sends a message into the output channel after a message in the input channel was received, with a delay of `duration`.
|
||||||
|
/// The delay is reset when a new message is reset.
|
||||||
|
pub fn spawn_debouncer(duration: Duration) -> (Sender<()>, Receiver<()>) {
|
||||||
|
let (input_sender, mut input_receiver) = mpsc::channel::<()>(1);
|
||||||
|
let (output_sender, output_receiver) = mpsc::channel::<()>(1);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
'outer: loop {
|
||||||
|
if input_receiver.recv().await.is_none() {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
'inner: loop {
|
||||||
|
match timeout(duration, input_receiver.recv()).await {
|
||||||
|
Ok(None) => break 'outer,
|
||||||
|
Ok(Some(_)) => continue 'inner,
|
||||||
|
Err(_) => {
|
||||||
|
if let Err(TrySendError::Closed(_)) = output_sender.try_send(()) {
|
||||||
|
break 'outer;
|
||||||
|
} else {
|
||||||
|
break 'inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(input_sender, output_receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_duration(duration: Duration) -> String {
|
||||||
|
let full_seconds = duration.as_secs();
|
||||||
|
let full_minutes = full_seconds / 60;
|
||||||
|
let hours = full_minutes / 60;
|
||||||
|
let minutes = full_minutes % 60;
|
||||||
|
let seconds = full_seconds % 60;
|
||||||
|
|
||||||
|
if hours == 0 {
|
||||||
|
format!("{:0>2}:{:0>2}", minutes, seconds)
|
||||||
|
} else {
|
||||||
|
format!("{:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_far_future() -> Instant {
|
||||||
|
Instant::now() + Duration::from_secs(60 * 60 * 24 * 365 * 30) // 30 years
|
||||||
|
}
|
|
@ -7,10 +7,11 @@ use resvg::usvg::tiny_skia_path::IntSize;
|
||||||
use rgb::RGB8;
|
use rgb::RGB8;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent};
|
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent, VibrationPattern};
|
||||||
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
||||||
use deckster_shared::state::{Key, Knob};
|
use deckster_shared::state::{Key, Knob};
|
||||||
use loupedeck_serial::characteristics::{LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
|
use loupedeck_serial::characteristics::{LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
|
||||||
|
use loupedeck_serial::commands::VibrationPattern as LSVibrationPattern;
|
||||||
use loupedeck_serial::device::LoupedeckDevice;
|
use loupedeck_serial::device::LoupedeckDevice;
|
||||||
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
|
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
|
||||||
|
|
||||||
|
@ -251,6 +252,9 @@ fn handle_handler_command(context: &mut IoWorkerContext, command: HandlerCommand
|
||||||
log::trace!("Handling handler command: {:?}", &command);
|
log::trace!("Handling handler command: {:?}", &command);
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
HandlerCommand::Vibrate { pattern } => {
|
||||||
|
context.device.vibrate(map_vibration_pattern(pattern));
|
||||||
|
}
|
||||||
HandlerCommand::SetKeyStyle { path, value } => {
|
HandlerCommand::SetKeyStyle { path, value } => {
|
||||||
context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
|
context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
|
||||||
k.style = value;
|
k.style = value;
|
||||||
|
@ -417,3 +421,36 @@ fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition {
|
||||||
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
|
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_vibration_pattern(value: VibrationPattern) -> LSVibrationPattern {
|
||||||
|
match value {
|
||||||
|
VibrationPattern::Short => LSVibrationPattern::Short,
|
||||||
|
VibrationPattern::Medium => LSVibrationPattern::Medium,
|
||||||
|
VibrationPattern::Long => LSVibrationPattern::Long,
|
||||||
|
VibrationPattern::Low => LSVibrationPattern::Low,
|
||||||
|
VibrationPattern::ShortLow => LSVibrationPattern::ShortLow,
|
||||||
|
VibrationPattern::ShortLower => LSVibrationPattern::ShortLower,
|
||||||
|
VibrationPattern::Lower => LSVibrationPattern::Lower,
|
||||||
|
VibrationPattern::Lowest => LSVibrationPattern::Lowest,
|
||||||
|
VibrationPattern::DescendSlow => LSVibrationPattern::DescendSlow,
|
||||||
|
VibrationPattern::DescendMed => LSVibrationPattern::DescendMed,
|
||||||
|
VibrationPattern::DescendFast => LSVibrationPattern::DescendFast,
|
||||||
|
VibrationPattern::AscendSlow => LSVibrationPattern::AscendSlow,
|
||||||
|
VibrationPattern::AscendMed => LSVibrationPattern::AscendMed,
|
||||||
|
VibrationPattern::AscendFast => LSVibrationPattern::AscendFast,
|
||||||
|
VibrationPattern::RevSlowest => LSVibrationPattern::RevSlowest,
|
||||||
|
VibrationPattern::RevSlow => LSVibrationPattern::RevSlow,
|
||||||
|
VibrationPattern::RevMed => LSVibrationPattern::RevMed,
|
||||||
|
VibrationPattern::RevFast => LSVibrationPattern::RevFast,
|
||||||
|
VibrationPattern::RevFaster => LSVibrationPattern::RevFaster,
|
||||||
|
VibrationPattern::RevFastest => LSVibrationPattern::RevFastest,
|
||||||
|
VibrationPattern::RiseFall => LSVibrationPattern::RiseFall,
|
||||||
|
VibrationPattern::Buzz => LSVibrationPattern::Buzz,
|
||||||
|
VibrationPattern::Rumble5 => LSVibrationPattern::Rumble5,
|
||||||
|
VibrationPattern::Rumble4 => LSVibrationPattern::Rumble4,
|
||||||
|
VibrationPattern::Rumble3 => LSVibrationPattern::Rumble3,
|
||||||
|
VibrationPattern::Rumble2 => LSVibrationPattern::Rumble2,
|
||||||
|
VibrationPattern::Rumble1 => LSVibrationPattern::Rumble1,
|
||||||
|
VibrationPattern::VeryLong => LSVibrationPattern::VeryLong,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use rumqttc::{Event, Incoming, LastWill, MqttOptions, QoS};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use crate::coordinator::io_worker::CoordinatorCommand;
|
use crate::coordinator::io_worker::CoordinatorCommand;
|
||||||
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent};
|
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, VibrationPattern};
|
||||||
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
||||||
use deckster_shared::style::{KeyStyle, KnobStyle};
|
use deckster_shared::style::{KeyStyle, KnobStyle};
|
||||||
|
|
||||||
|
@ -132,6 +132,13 @@ pub async fn start_mqtt_client(
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
"vibrate" => {
|
||||||
|
if let Ok(pattern) = serde_json::from_slice::<VibrationPattern>(&event.payload) {
|
||||||
|
handler_commands_sender.send_async(HandlerCommand::Vibrate { pattern }).await.unwrap();
|
||||||
|
} else {
|
||||||
|
log::error!("Unknown vibration pattern from {}: {}", event.topic, String::from_utf8_lossy(&event.payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
"keys" => {
|
"keys" => {
|
||||||
let page_id = segments[1];
|
let page_id = segments[1];
|
||||||
let position = segments[2];
|
let position = segments[2];
|
||||||
|
|
|
@ -70,7 +70,11 @@ impl State {
|
||||||
active_key_page_id: config.initial.key_page.clone(),
|
active_key_page_id: config.initial.key_page.clone(),
|
||||||
active_knob_page_id: config.initial.knob_page.clone(),
|
active_knob_page_id: config.initial.knob_page.clone(),
|
||||||
active_touch_ids: HashSet::new(),
|
active_touch_ids: HashSet::new(),
|
||||||
active_remote_handler_host_ids: HashSet::new(),
|
active_remote_handler_host_ids: {
|
||||||
|
let mut s = HashSet::new();
|
||||||
|
s.insert("".to_owned().into_boxed_str());
|
||||||
|
s
|
||||||
|
},
|
||||||
key_pages_by_id,
|
key_pages_by_id,
|
||||||
knob_pages_by_id,
|
knob_pages_by_id,
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ pub async fn start_mqtt_client(
|
||||||
is_first_try = false;
|
is_first_try = false;
|
||||||
|
|
||||||
client.subscribe(format!("{topic_prefix}/config"), QoS::ExactlyOnce).await.unwrap();
|
client.subscribe(format!("{topic_prefix}/config"), QoS::ExactlyOnce).await.unwrap();
|
||||||
|
client.subscribe(format!("{topic_prefix}/vibrate"), QoS::ExactlyOnce).await.unwrap();
|
||||||
client.subscribe(format!("{topic_prefix}/keys/+/+/events"), QoS::ExactlyOnce).await.unwrap();
|
client.subscribe(format!("{topic_prefix}/keys/+/+/events"), QoS::ExactlyOnce).await.unwrap();
|
||||||
client.subscribe(format!("{topic_prefix}/knobs/+/+/events"), QoS::ExactlyOnce).await.unwrap();
|
client.subscribe(format!("{topic_prefix}/knobs/+/+/events"), QoS::ExactlyOnce).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -72,7 +73,8 @@ pub async fn start_mqtt_client(
|
||||||
let topic_name = event.topic;
|
let topic_name = event.topic;
|
||||||
let topic_segments = topic_name.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::<Vec<&str>>();
|
let topic_segments = topic_name.strip_prefix(&topic_prefix).unwrap().split('/').skip(1).collect::<Vec<&str>>();
|
||||||
|
|
||||||
if topic_segments[0] == "config" {
|
match topic_segments[0] {
|
||||||
|
"config" => {
|
||||||
if let Ok(config) = serde_json::from_slice::<Option<HandlerHostsConfig>>(&event.payload) {
|
if let Ok(config) = serde_json::from_slice::<Option<HandlerHostsConfig>>(&event.payload) {
|
||||||
if let Some(c) = &config {
|
if let Some(c) = &config {
|
||||||
client
|
client
|
||||||
|
@ -86,7 +88,8 @@ pub async fn start_mqtt_client(
|
||||||
log::error!("Could not deserialize the latest configuration from {}", topic_name);
|
log::error!("Could not deserialize the latest configuration from {}", topic_name);
|
||||||
handler_hosts_config_sender.send(None).await.unwrap();
|
handler_hosts_config_sender.send(None).await.unwrap();
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
_ => {
|
||||||
let page_id = topic_segments[1];
|
let page_id = topic_segments[1];
|
||||||
let position = topic_segments[2];
|
let position = topic_segments[2];
|
||||||
let property = topic_segments[3];
|
let property = topic_segments[3];
|
||||||
|
@ -134,6 +137,7 @@ pub async fn start_mqtt_client(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,6 +150,15 @@ pub async fn start_mqtt_client(
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(command) = commands_receiver.recv_async().await {
|
while let Ok(command) = commands_receiver.recv_async().await {
|
||||||
match command {
|
match command {
|
||||||
|
HandlerCommand::Vibrate { pattern } => client
|
||||||
|
.publish(
|
||||||
|
format!("{topic_prefix}/vibrate"),
|
||||||
|
QoS::ExactlyOnce,
|
||||||
|
false,
|
||||||
|
serde_json::to_vec(&pattern).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
HandlerCommand::SetKeyStyle { path, value } => client
|
HandlerCommand::SetKeyStyle { path, value } => client
|
||||||
.publish(
|
.publish(
|
||||||
format!("{topic_prefix}/keys/{path}/style"),
|
format!("{topic_prefix}/keys/{path}/style"),
|
||||||
|
|
Loading…
Add table
Reference in a new issue