feat(asusd): Implement threaded Aura animator and hardware coordination

This commit is contained in:
mihai2mn
2026-01-24 16:53:08 +01:00
parent 4c0d054deb
commit 55b7c24556
4 changed files with 269 additions and 0 deletions

View File

@@ -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<Mutex<AnimationMode>>,
/// Flag to stop the animation
pub stop: Arc<AtomicBool>,
/// Flag indicating if the thread is currently running
pub running: Arc<AtomicBool>,
}
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<AnimatorState>) {
// 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");
});
}

View File

@@ -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<Arc<Mutex<HidRaw>>>,
pub backlight: Option<Arc<Mutex<KeyboardBacklight>>>,
pub config: Arc<Mutex<AuraConfig>>,
/// Animation state for software-controlled effects
pub animator: Arc<animator::AnimatorState>,
}
impl Aura {

View File

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

View File

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