commit
This commit is contained in:
parent
566ffb49f3
commit
13f307d387
12 changed files with 543 additions and 69 deletions
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue