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

20
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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> },

View file

@ -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
View 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"

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
}

View file

@ -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,
}
}

View file

@ -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];

View file

@ -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,
} }

View file

@ -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,65 +73,68 @@ 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] {
if let Ok(config) = serde_json::from_slice::<Option<HandlerHostsConfig>>(&event.payload) { "config" => {
if let Some(c) = &config { if let Ok(config) = serde_json::from_slice::<Option<HandlerHostsConfig>>(&event.payload) {
client if let Some(c) = &config {
.publish(activeness_topic.clone(), QoS::AtLeastOnce, true, c.run_id.to_owned().into_boxed_bytes()) client
.await .publish(activeness_topic.clone(), QoS::AtLeastOnce, true, c.run_id.to_owned().into_boxed_bytes())
.unwrap(); .await
} .unwrap();
}
handler_hosts_config_sender.send(config).await.unwrap(); handler_hosts_config_sender.send(config).await.unwrap();
} else { } else {
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 position = topic_segments[2]; let page_id = topic_segments[1];
let property = topic_segments[3]; let position = topic_segments[2];
let property = topic_segments[3];
match topic_segments[0] { match topic_segments[0] {
"keys" if property == "events" => { "keys" if property == "events" => {
if let Ok(event) = serde_json::from_slice(&event.payload) { if let Ok(event) = serde_json::from_slice(&event.payload) {
if let Ok(position) = KeyPosition::from_str(position) { if let Ok(position) = KeyPosition::from_str(position) {
// This can be Err when events are received before the configuration // This can be Err when events are received before the configuration
// but in that case we just ignore the event. // but in that case we just ignore the event.
_ = events_sender.send(HandlerEvent::Key { _ = events_sender.send(HandlerEvent::Key {
path: KeyPath { path: KeyPath {
page_id: page_id.to_owned(), page_id: page_id.to_owned(),
position, position,
}, },
event, event,
}) })
} else {
log::warn!("Invalid key position in topic name: {topic_name}");
}
} else { } else {
log::warn!("Invalid key position in topic name: {topic_name}"); log::error!("Could not deserialize the latest event from {topic_name}");
} };
} else { }
log::error!("Could not deserialize the latest event from {topic_name}"); "knobs" if property == "events" => {
}; if let Ok(event) = serde_json::from_slice(&event.payload) {
} if let Ok(position) = KnobPosition::from_str(position) {
"knobs" if property == "events" => { // This can be Err when events are received before the configuration
if let Ok(event) = serde_json::from_slice(&event.payload) { // but in that case we just ignore the event.
if let Ok(position) = KnobPosition::from_str(position) { _ = events_sender.send(HandlerEvent::Knob {
// This can be Err when events are received before the configuration path: KnobPath {
// but in that case we just ignore the event. page_id: page_id.to_owned(),
_ = events_sender.send(HandlerEvent::Knob { position,
path: KnobPath { },
page_id: page_id.to_owned(), event,
position, });
}, } else {
event, log::warn!("Invalid knob position in topic name: {topic_name}");
}); }
} else { } else {
log::warn!("Invalid knob position in topic name: {topic_name}"); log::error!("Could not deserialize the latest event from {}", topic_name);
} };
} else { }
log::error!("Could not deserialize the latest event from {}", topic_name); _ => {}
};
} }
_ => {}
} }
} }
} }
@ -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"),