This commit is contained in:
Moritz Ruth 2024-03-04 12:29:44 +01:00
parent 566ffb49f3
commit 13f307d387
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
12 changed files with 543 additions and 69 deletions

View 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,
});
}

View 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(())
}

View 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
}