From 55b7c245568e9eef869a3b69a57ef28b90000ab0 Mon Sep 17 00:00:00 2001 From: mihai2mn Date: Sat, 24 Jan 2026 16:53:08 +0100 Subject: [PATCH] feat(asusd): Implement threaded Aura animator and hardware coordination --- asusd/src/aura_laptop/animator.rs | 199 +++++++++++++++++++++++++++ asusd/src/aura_laptop/mod.rs | 3 + asusd/src/aura_laptop/trait_impls.rs | 66 +++++++++ asusd/src/aura_types.rs | 1 + 4 files changed, 269 insertions(+) create mode 100644 asusd/src/aura_laptop/animator.rs diff --git a/asusd/src/aura_laptop/animator.rs b/asusd/src/aura_laptop/animator.rs new file mode 100644 index 00000000..a2e51413 --- /dev/null +++ b/asusd/src/aura_laptop/animator.rs @@ -0,0 +1,199 @@ +//! Animation task runner for asusd daemon. +//! +//! This module provides the background thread that runs LED animations. +//! Animations persist even when the GUI is closed. +//! +//! Note: Uses std::thread and blocking for stability across contexts. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use log::{debug, info, warn}; +use rog_aura::animation::{apply_brightness, hsv_to_rgb, lerp_colour, AnimationMode}; +use rog_aura::{AuraEffect, AuraModeNum, Colour}; + +use super::Aura; + +/// State for the animation task +#[derive(Debug)] +pub struct AnimatorState { + /// Current animation mode + pub mode: Arc>, + /// Flag to stop the animation + pub stop: Arc, + /// Flag indicating if the thread is currently running + pub running: Arc, +} + +impl AnimatorState { + pub fn new() -> Self { + Self { + mode: Arc::new(Mutex::new(AnimationMode::None)), + stop: Arc::new(AtomicBool::new(false)), + running: Arc::new(AtomicBool::new(false)), + } + } + + /// Signal the animator to stop + pub fn signal_stop(&self) { + self.stop.store(true, Ordering::Relaxed); + } + + /// Check if stop signal is set + pub fn should_stop(&self) -> bool { + self.stop.load(Ordering::Relaxed) + } + + /// Clear the stop flag + pub fn clear_stop(&self) { + self.stop.store(false, Ordering::Relaxed); + } + + /// Check if animator thread is running + pub fn is_running(&self) -> bool { + self.running.load(Ordering::Relaxed) + } +} + +pub fn spawn_animator(aura: Aura, state: Arc) { + // Mark as running + state.running.store(true, Ordering::Relaxed); + + thread::spawn(move || { + info!("Aura animator thread started"); + + // Animation state variables + let mut hue: f32 = 0.0; // For Rainbow + let mut color_index: usize = 0; // For ColorCycle + let mut hold_counter: u32 = 0; // For ColorCycle hold + let mut lerp_t: f32 = 0.0; // Interpolation factor + let mut breathe_phase: f32 = 0.0; // For Breathe (0-2π) + let mut pulse_phase: f32 = 0.0; // For Pulse (0-2π) + + loop { + // Check stop flag + if state.should_stop() { + debug!("Animator received stop signal"); + break; + } + + // Get current mode (with timeout to verify loop health) + let mode_opt = if let Ok(guard) = state.mode.lock() { + Some(guard.clone()) + } else { + None + }; + + let mode = match mode_opt { + Some(m) => m, + None => { + warn!("Failed to lock mode mutex"); + thread::sleep(Duration::from_millis(100)); + continue; + } + }; + + if !mode.is_active() { + // No animation, sleep briefly and check again + thread::sleep(Duration::from_millis(100)); + continue; + } + + let speed_ms = mode.speed_ms().max(50); // Minimum 50ms interval + + // Generate the color for this frame + let color = match &mode { + AnimationMode::None => continue, + + AnimationMode::Rainbow { .. } => { + hue = (hue + 5.0) % 360.0; + hsv_to_rgb(hue, 1.0, 1.0) + } + + AnimationMode::ColorCycle { colors, .. } => { + if colors.is_empty() { + Colour { r: 255, g: 0, b: 0 } + } else if hold_counter > 0 { + hold_counter -= 1; + colors[color_index] + } else { + let next_index = (color_index + 1) % colors.len(); + lerp_t += 0.05; // Fade speed + + if lerp_t >= 1.0 { + lerp_t = 0.0; + color_index = next_index; + hold_counter = 20; // Hold for ~1-2 seconds (20 * speed_ms) + colors[color_index] // Ensure we land exactly on target + } else { + lerp_colour(&colors[color_index], &colors[next_index], lerp_t) + } + } + } + + AnimationMode::Breathe { color1, color2, .. } => { + breathe_phase += 0.05; // Slow smooth breathe + if breathe_phase > std::f32::consts::TAU { + breathe_phase = 0.0; + } + + // Smooth sine wave breathe: C1 -> Black -> C2 -> Black -> C1 + // 0..PI: Pulse C1 + // PI..2PI: Pulse C2 + + if breathe_phase < std::f32::consts::PI { + let brightness = (breathe_phase.sin()).abs(); + apply_brightness(*color1, brightness) + } else { + let brightness = ((breathe_phase - std::f32::consts::PI).sin()).abs(); + apply_brightness(*color2, brightness) + } + } + + AnimationMode::Pulse { + color, + min_brightness, + max_brightness, + .. + } => { + pulse_phase += 0.1; + if pulse_phase > std::f32::consts::TAU { + pulse_phase = 0.0; + } + + // Sine wave between min and max brightness + let t = (pulse_phase.sin() + 1.0) / 2.0; // 0-1 + let brightness = min_brightness + (max_brightness - min_brightness) * t; + apply_brightness(*color, brightness) + } + }; + + // Apply the color to the LED using async call directly + let effect = AuraEffect { + mode: AuraModeNum::Static, + colour1: color, + ..Default::default() + }; + + // Execute async code block synchronously using futures_lite + let res = futures_lite::future::block_on(async { + let config = aura.config.lock().await; + let dev_type = config.led_type; + drop(config); + + aura.write_effect_and_apply(dev_type, &effect).await + }); + + if let Err(e) = res { + warn!("Animation frame failed: {:?}", e); + } + + thread::sleep(Duration::from_millis(speed_ms as u64)); + } + + state.running.store(false, Ordering::Relaxed); + info!("Aura animator thread stopped"); + }); +} diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 7fabceba..aaf7517d 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -12,6 +12,7 @@ use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; +pub mod animator; pub mod config; pub mod trait_impls; @@ -20,6 +21,8 @@ pub struct Aura { pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, + /// Animation state for software-controlled effects + pub animator: Arc, } impl Aura { diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index db10d166..36b935cc 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -131,6 +131,7 @@ impl AuraZbus { /// the effect is stored and config written to disk. #[zbus(property)] async fn set_led_mode(&mut self, num: AuraModeNum) -> Result<(), ZbErr> { + self.0.animator.signal_stop(); let mut config = self.0.config.lock().await; config.current_mode = num; self.0.write_current_config_mode(&mut config).await?; @@ -163,6 +164,7 @@ impl AuraZbus { /// the effect is stored and config written to disk. #[zbus(property)] async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> { + self.0.animator.signal_stop(); let mut config = self.0.config.lock().await; if !config.support_data.basic_modes.contains(&effect.mode) || effect.zone != AuraZone::None @@ -229,6 +231,70 @@ impl AuraZbus { self.0.write_effect_block(&mut config, &data).await?; Ok(()) } + + /// Start a software-controlled animation. + /// Animations run in the daemon and persist when GUI is closed. + /// `mode_json` is a JSON-serialized AnimationMode. + async fn start_animation(&self, mode_json: String) -> Result<(), ZbErr> { + // Deserialize the mode from JSON + let mode: rog_aura::AnimationMode = serde_json::from_str(&mode_json) + .map_err(|e| ZbErr::Failed(format!("Invalid animation mode JSON: {}", e)))?; + + // Stop any existing animation first + self.0.animator.signal_stop(); + + // Wait for previous thread to stop + // Check for up to 1 second + for _ in 0..20 { + if !self.0.animator.is_running() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + + // Set new mode and clear stop flag (using std::sync::Mutex) + if let Ok(mut guard) = self.0.animator.mode.lock() { + *guard = mode.clone(); + } + self.0.animator.clear_stop(); + + // Spawn the animation thread + if mode.is_active() { + super::animator::spawn_animator(self.0.clone(), self.0.animator.clone()); + info!("Started animation: {:?}", mode); + } + Ok(()) + } + + /// Stop any running animation + async fn stop_animation(&self) -> Result<(), ZbErr> { + self.0.animator.signal_stop(); + if let Ok(mut guard) = self.0.animator.mode.lock() { + *guard = rog_aura::AnimationMode::None; + } + info!("Stopped animation"); + Ok(()) + } + + /// Check if an animation is currently running + #[zbus(property)] + async fn animation_running(&self) -> bool { + if let Ok(mode) = self.0.animator.mode.lock() { + mode.is_active() && !self.0.animator.should_stop() + } else { + false + } + } + + /// Get the current animation mode as JSON + #[zbus(property)] + async fn animation_mode(&self) -> String { + if let Ok(mode) = self.0.animator.mode.lock() { + serde_json::to_string(&*mode).unwrap_or_else(|_| "\"None\"".to_string()) + } else { + "\"None\"".to_string() + } + } } impl CtrlTask for AuraZbus { diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 3a8cf12d..86d453cb 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -202,6 +202,7 @@ impl DeviceHandle { hid: device, backlight, config: Arc::new(Mutex::new(config)), + animator: Arc::new(crate::aura_laptop::animator::AnimatorState::new()), }; aura.do_initialization().await?; Ok(Self::Aura(aura))