From 41abc3f67cf09196ffefd0d5b3c1d862590e2a24 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 11 Jun 2026 09:04:46 +0100 Subject: [PATCH] Refresh Move lights without blocking Muninn --- crates/muninn-daemon/src/main.rs | 182 +++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 43 deletions(-) diff --git a/crates/muninn-daemon/src/main.rs b/crates/muninn-daemon/src/main.rs index d96b4b5..774180c 100644 --- a/crates/muninn-daemon/src/main.rs +++ b/crates/muninn-daemon/src/main.rs @@ -10,7 +10,7 @@ use std::io::Write; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::thread; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[derive(Clone, Debug, PartialEq, Eq)] enum Mode { @@ -59,6 +59,14 @@ struct MuxPlan { targets: Vec, } +struct ActiveMoveLightCommand { + command: MuninnMoveLightCommandRecord, + colors: Vec<(u8, u8, u8)>, + step_index: usize, + repeats_done: u32, + next_write_at: Instant, +} + trait ProcessSpawner { fn spawn_mux(&self, plan: &MuxPlan) -> Result; } @@ -97,16 +105,22 @@ fn main() -> Result<()> { fn serve(options: Options) -> Result<()> { ensure_state_dirs(&options)?; + let mut active_move_lights = Vec::new(); loop { let mut node = open_node(&options, "muninn-daemon")?; - process_move_light_commands(&mut node, &options, &mut HidMoveLightWriter)?; + register_move_light_commands(&mut node, &options, &mut active_move_lights)?; + tick_move_light_commands(&mut node, &mut active_move_lights, &mut HidMoveLightWriter)?; publish_surface(&mut node, &options, "idle", &[])?; - if let Some(interval) = options.interval_seconds { - thread::sleep(Duration::from_secs(interval)); - } else { + if options.interval_seconds.is_none() && active_move_lights.is_empty() { return Ok(()); } + let sleep = if active_move_lights.is_empty() { + Duration::from_secs(options.interval_seconds.unwrap_or(15)) + } else { + Duration::from_millis(250) + }; + thread::sleep(sleep); } } @@ -325,27 +339,131 @@ impl MoveLightWriter for HidMoveLightWriter { } } -fn process_move_light_commands( +fn register_move_light_commands( node: &mut cultmesh_rs::CultMeshNode, options: &Options, - writer: &mut impl MoveLightWriter, + active: &mut Vec, ) -> Result<()> { let commands = node.cache().get_all::()?; for command in commands { - if command.host_id != options.host_id || command.state != "pending" { + if command.host_id != options.host_id { + continue; + } + if command.state != "pending" + && !(command.state == "running" + && !active + .iter() + .any(|active| active.command.command_id == command.command_id)) + { continue; } + let active_command = match prepare_move_light_command(command.clone()) { + Ok(active_command) => active_command, + Err(error) => { + let command_id = command.command_id.clone(); + node.put(&command_id, &command_failed(command, &error.to_string())?)?; + continue; + } + }; let running = MuninnMoveLightCommandRecord { state: "running".to_string(), - detail: "Muninn is writing local PS Move HID reports.".to_string(), + detail: "Muninn is refreshing local PS Move HID reports.".to_string(), updated_at: timestamp()?, ..command.clone() }; node.put(&running.command_id, &running)?; + active.push(ActiveMoveLightCommand { + command: running, + ..active_command + }); + } + Ok(()) +} - let result = execute_move_light_command(running, writer)?; - node.put(&result.command_id, &result)?; +fn prepare_move_light_command( + command: MuninnMoveLightCommandRecord, +) -> Result { + let colors = parse_move_colors(&command.colors)?; + if command.hidraw_path.trim().is_empty() { + return Err(anyhow!("hidraw_path is required")); + } + if command.repeat_count == 0 { + return Err(anyhow!("repeat_count must be greater than zero")); + } + if command.repeat_count > 86_400 { + return Err(anyhow!("repeat_count must be 86400 or less")); + } + if command.durations_ms.len() != colors.len() { + return Err(anyhow!( + "durations_ms must contain one duration for each color" + )); + } + if command + .durations_ms + .iter() + .any(|duration| *duration > 60_000) + { + return Err(anyhow!("durations_ms values must be 60000 or less")); + } + + Ok(ActiveMoveLightCommand { + command, + colors, + step_index: 0, + repeats_done: 0, + next_write_at: Instant::now(), + }) +} + +fn tick_move_light_commands( + node: &mut cultmesh_rs::CultMeshNode, + active: &mut Vec, + writer: &mut impl MoveLightWriter, +) -> Result<()> { + let now = Instant::now(); + let mut index = 0; + while index < active.len() { + if active[index].next_write_at > now { + index += 1; + continue; + } + + let command_id = active[index].command.command_id.clone(); + let (red, green, blue) = active[index].colors[active[index].step_index]; + let report = [0x06, 0, red, green, blue, 0, 0, 0, 0]; + if let Err(error) = writer.write_report(&active[index].command.hidraw_path, &report) { + let failed = command_failed(active[index].command.clone(), &format!("{error:#}"))?; + node.put(&command_id, &failed)?; + active.remove(index); + continue; + } + + let duration_ms = active[index].command.durations_ms[active[index].step_index]; + active[index].step_index += 1; + if active[index].step_index >= active[index].colors.len() { + active[index].step_index = 0; + active[index].repeats_done += 1; + } + + if active[index].repeats_done >= active[index].command.repeat_count { + let completed = MuninnMoveLightCommandRecord { + state: "completed".to_string(), + detail: format!( + "wrote {} PS Move light step(s) for {} repeat(s)", + active[index].colors.len(), + active[index].command.repeat_count + ), + updated_at: timestamp()?, + ..active[index].command.clone() + }; + node.put(&command_id, &completed)?; + active.remove(index); + } else { + active[index].next_write_at = + now + Duration::from_millis(u64::from(duration_ms.max(1))); + index += 1; + } } Ok(()) } @@ -354,40 +472,18 @@ fn execute_move_light_command( command: MuninnMoveLightCommandRecord, writer: &mut impl MoveLightWriter, ) -> Result { - let colors = match parse_move_colors(&command.colors) { - Ok(colors) => colors, + let active = match prepare_move_light_command(command.clone()) { + Ok(active) => active, Err(error) => return command_failed(command, &error.to_string()), }; - if command.hidraw_path.trim().is_empty() { - return command_failed(command, "hidraw_path is required"); - } - if command.repeat_count == 0 { - return command_failed(command, "repeat_count must be greater than zero"); - } - if command.repeat_count > 1024 { - return command_failed(command, "repeat_count must be 1024 or less"); - } - if command.durations_ms.len() != colors.len() { - return command_failed( - command, - "durations_ms must contain one duration for each color", - ); - } - if command - .durations_ms - .iter() - .any(|duration| *duration > 60_000) - { - return command_failed(command, "durations_ms values must be 60000 or less"); - } - for _ in 0..command.repeat_count { - for (index, (red, green, blue)) in colors.iter().copied().enumerate() { + for _ in 0..active.command.repeat_count { + for (index, (red, green, blue)) in active.colors.iter().copied().enumerate() { let report = [0x06, 0, red, green, blue, 0, 0, 0, 0]; - if let Err(error) = writer.write_report(&command.hidraw_path, &report) { - return command_failed(command, &format!("{error:#}")); + if let Err(error) = writer.write_report(&active.command.hidraw_path, &report) { + return command_failed(active.command, &format!("{error:#}")); } - let duration_ms = command.durations_ms[index]; + let duration_ms = active.command.durations_ms[index]; if duration_ms > 0 { thread::sleep(Duration::from_millis(u64::from(duration_ms))); } @@ -398,11 +494,11 @@ fn execute_move_light_command( state: "completed".to_string(), detail: format!( "wrote {} PS Move light step(s) for {} repeat(s)", - colors.len(), - command.repeat_count + active.colors.len(), + active.command.repeat_count ), updated_at: timestamp()?, - ..command + ..active.command }) }