diff --git a/CHANGELOG.md b/CHANGELOG.md index 00725caf..c50b5955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ ## [6.3.0] ### Changed + +## [6.4.0] + +### Added + +- Migrated Aura animations (Rainbow, Pulse, Breathe, Color Cycle) to asusd daemon +- Thread-based animator in daemon for stability and smoothness +- DBus interface for controlling animations + +### Changed + +- Refactored ROGCC to control animations via DBus +- Removed legacy client-side animation code +- Improved Breathe and Color Cycle visual logic + +## [6.3.0] + +### Changed + - Added support for TUF keyboard powerstate control - Improved AniMe Matrix support thanks to @Seom1177 ! - Fixed a bug with one-shot battery change, thanks @bitr8 ! @@ -12,6 +31,7 @@ ## [6.2.0] ### Changed + - Added aura support for FX607V: thanks @jomp16 - Added testing support for G835LW - Added support for GU605C models slash lighting: thanks @Otters @@ -21,6 +41,7 @@ ## [6.1.22] ### Changed + - Allow configuration of nv_tgp - Treat dGPU attributes as power profiles - Add EXPERTBOOK DMI match to ensure the service is loaded @@ -29,49 +50,57 @@ ## [6.1.21] ### Changed + - Kill Fedora: screw your cursed cargo bullshit - Restore CI building ## [6.1.20] ### Changed + - Addded support for G635L: thanks @luca_pisl ! - Suppress verbose output in applications too, not just daemon ## [6.1.18] ### Changed + - Add aura support for G614FR (ROG Strix G16 2025) -- all notifications now respects the timeout +- all notifications now respects the timeout - improve udev daemon-starting rule - reduce log noise ## [v6.1.17] ### Changed + - Fix Makefile - Share a single HID device ## [v6.1.16] ### Changed + - Expose more properties via rog-control-center - Add support for a few more models ## [v6.1.15] ### Changed + - Reflect the current asus-armoury status on AC plug connection status change ## [v6.1.14] ### Changed + - Fix formatting - Attempt to fix tests ## [v6.1.13] ### Changed + - Fix a problem in reloading the service (@evertvorster) - Add Azerbaijani language (@rashadgasimli) - Add Ubuntu installation instructions @@ -79,31 +108,37 @@ ## [v6.1.12] ### Changed + - Fix an unbounded event loop caused by other processes causing a "modify" event on the screen backlight brightness. ## [v6.1.11] ### Changed + - Fix anime flickering issue when using custom anims (@I-Al-Istannen) - Include pt_BR translations file (@PabloKiryu) ## Added + - Support for the screenpad brightness on some Laptops. This includes syncing to the primary screen brightness, and a gamma adjustment to set brightness scaling. - Add asusctl CLI options - Add UI options - Add a fake gamma correction (`asusctl backlight --sync-screenpad-brightness`, 1.5 for example sets screenpad low brightness lower than primary, and scales upwards) ### Changed + - asusd: single line fix for profile switching ## [v6.1.9] ### Changed + - ROGCC: better handling of platform profiles ## [v6.1.8] ### Changed + - Testing CI for opensuse RPM build - ROGCC: Fixes to showing the PPT enablement toggle - ROGCC: Fixes to how PPT and NV sliders work and enable/disable @@ -112,43 +147,51 @@ ## [v6.1.7] ### Changed + - Fix Slash display enable ## [v6.1.6] ### Changed + - Disable skia bindings for UI again. It causes failures in build pipelines and requires extra dependencies. ## [v6.1.5] ### Changed + - Update dependencies - Fix fan-curve proxy type signatures ## [v6.1.4] ### Changed + - Fix git doing me a dirty ## [v6.1.3] ### Changed + - Many small bugfixes such as for platform profile switching ## [v6.1.2] ### Changed + - Try a slightly different tact to fix charge control slider ## [v6.1.1] ### Changed + - Fix aura data matching - Fix charge control slider ## [v6.1.0] ### Changed + - Update deps - Add support for G513RC RGB modes - Many UI fixes @@ -166,12 +209,14 @@ ## [v6.1.0-rc6] ### Changed + - Two small fixes, one for `low-power` profile name, and one for base gpu tdp - Move to using platform_profile api only (no throttle_thermal_policy) ## [v6.1.0-rc5] ### Changed + - Per-AC/DC, per-profile tunings enabled (Battery vs AC power + platform profile) - Add ability to restore PPT defaults - Add PPT help dialogue to UI @@ -180,6 +225,7 @@ ## [v6.1.0-rc4] ### Changed + - Bug fix: UI was setting incorrect value for FPPT - Bug fix: Re-add callbacks for the throttle and epp settings in UI - Bug fix: Fix UI settigns for AniMe Matrix display @@ -190,23 +236,27 @@ ## [v6.1.0-rc3] ### Changed + - Bug fixes - Partial support for per-profile CPU tunings (WIP) ## [v6.1.0-rc2] ### Added + - asus-armoury driver support. WIP, will be adjusted/changed further - More "Slash" display controls ## [v6.1.0-rc1] ### Added + - ROG Arion external driver LED support - Add GA605W LED layout - Add GA605 + GU605 Slash support ### Changed + - Fix attribute writes. At some point the kernel API seems to have changed. - Extremely large refactor of Aura device handling. Should enable easy add of different kinds now. - Rename CLI args for aura related properties. This will likely change further as more devices are added @@ -214,6 +264,7 @@ ## [v6.0.12] ### Changed + - Add Ally X aura config - Fixes to Ally led power configs - Fix CLI led modes @@ -224,6 +275,7 @@ ## [v6.0.11] ### Changed + - Renamed `Strobe` effect to `RainbowCycle` to prevent confusion over what it is - Ranamed `Rainbow` effect to `RainbowWave` - Cleaned up serde crate deps @@ -234,6 +286,7 @@ ## [v6.0.10] ### Added + - Add the GA401I model to aura_support. ### Changed @@ -263,12 +316,15 @@ ## [v6.0.8] ### Added + - Add G512L laptop DB entry ### Changed + - Add more tests to verify things ### Fix + - asusctl incorrectly assumes fan-curves unsupported. Now fixed. - try to fix ROGCC using CPU time. @@ -285,9 +341,11 @@ ## [v6.0.6] ### Added + - Add GX650R laptop to aura DB ### Changed + - Further tweaks to aura init - More logging - Fix TUF laptop led power @@ -349,7 +407,7 @@ ### Important note -- The kernel patches from [here](https://lore.kernel.org/platform-driver-x86/20240404001652.86207-1-luke@ljones.dev/) are required. The ppt settings _will_ still apply without the patches but will be called a fail due to the read-back not being implemented (solved with kernel patch). These patches have been upstreamed for kernel 6.10 +- The kernel patches from [here](https://lore.kernel.org/platform-driver-x86/20240404001652.86207-1-luke@ljones.dev/) are required. The ppt settings *will* still apply without the patches but will be called a fail due to the read-back not being implemented (solved with kernel patch). These patches have been upstreamed for kernel 6.10 - Z13 devices will need these Z13 devices will need [these](https://lore.kernel.org/linux-input/20240416090402.31057-1-luke@ljones.dev/T/#t) ### Changed @@ -369,7 +427,7 @@ ### BREAKING -- The aura dbus interface, and well pretty much all dbus interfaces have been changed. The Aura interface in particular works differently to begin implementing _multiple_ aura device support, including _hot-plug_ of devices (USB Aura keybords and others). +- The aura dbus interface, and well pretty much all dbus interfaces have been changed. The Aura interface in particular works differently to begin implementing *multiple* aura device support, including *hot-plug* of devices (USB Aura keybords and others). - All dbus interfaces except Aura are now in the `/org/asuslinux/` path - Aura dbus now appear under `/org/asuslinux/` and there may be multiple devices. To find these device you use the `ObjectManager` interface under the `/org/asuslinux` path. @@ -520,7 +578,7 @@ - Builtin animations - In-progress simulators for GA402, GU604 animatrix, optional build and takes a single arg - Add `model_override` option to anime config, this is handy for forcing a model for "Unknown" anime, and for simulators -- Add `mini_led_mode` support to asusd and zbus crates (requires kernel patch https://lkml.org/lkml/2023/6/19/1264) +- Add `mini_led_mode` support to asusd and zbus crates (requires kernel patch ) - Add `mini_led_mode` toggle to rog-control-center GUI, tray, notifications - Add generation of typescript types from the rust types used via dbus using typeshare - Add generation of introspection XML from asusd dbus @@ -959,7 +1017,7 @@ ### BREAKING CHANGES - Graphics control: - - graphics control is pulled out of asusd and moved to new package; https://gitlab.com/asus-linux/supergfxctl + - graphics control is pulled out of asusd and moved to new package; - Proflies: - profiles now depend on power-profile-daemon plus kernel patches for support of platform_profile - if your system supports fan-curves you will also require upcoming kernel patches for this @@ -998,7 +1056,7 @@ - Added ability to fade in/out gifs and images for anime. This does break anime configs. See manual for details. - Added task to CtrlLed to set the keyboard LED brightness on wake from suspend - requires a kernel patch which will be upstreamed and in fedora rog kernel -- Make gfx change from nvidia to vfio/compute also force-change to integrated _then_ +- Make gfx change from nvidia to vfio/compute also force-change to integrated *then* to requested mode - Fix invalid gfx status when switching from some modes - Fix copy over of serde skipped config values on config reload diff --git a/Cargo.lock b/Cargo.lock index 78c4d320..eef9d39c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "asusctl" -version = "6.3.0" +version = "6.4.0" dependencies = [ "argh", "dmi_id", @@ -232,7 +232,7 @@ dependencies = [ [[package]] name = "asusd" -version = "6.3.0" +version = "6.4.0" dependencies = [ "cargo-husky", "concat-idents", @@ -252,6 +252,7 @@ dependencies = [ "rog_scsi", "rog_slash", "serde", + "serde_json", "tokio", "udev 0.8.0", "zbus", @@ -259,7 +260,7 @@ dependencies = [ [[package]] name = "asusd-user" -version = "6.3.0" +version = "6.4.0" dependencies = [ "config-traits", "dirs", @@ -938,7 +939,7 @@ dependencies = [ [[package]] name = "config-traits" -version = "6.3.0" +version = "6.4.0" dependencies = [ "log", "ron", @@ -1274,7 +1275,7 @@ dependencies = [ [[package]] name = "dmi_id" -version = "6.3.0" +version = "6.4.0" dependencies = [ "log", "udev 0.8.0", @@ -4482,7 +4483,7 @@ dependencies = [ [[package]] name = "rog-control-center" -version = "6.3.0" +version = "6.4.0" dependencies = [ "asusd", "concat-idents", @@ -4505,6 +4506,7 @@ dependencies = [ "rog_slash", "ron", "serde", + "serde_json", "slint", "slint-build", "supergfxctl", @@ -4515,7 +4517,7 @@ dependencies = [ [[package]] name = "rog_anime" -version = "6.3.0" +version = "6.4.0" dependencies = [ "dmi_id", "gif 0.12.0", @@ -4529,7 +4531,7 @@ dependencies = [ [[package]] name = "rog_aura" -version = "6.3.0" +version = "6.4.0" dependencies = [ "dmi_id", "log", @@ -4540,7 +4542,7 @@ dependencies = [ [[package]] name = "rog_dbus" -version = "6.3.0" +version = "6.4.0" dependencies = [ "asusd", "rog_anime", @@ -4554,7 +4556,7 @@ dependencies = [ [[package]] name = "rog_platform" -version = "6.3.0" +version = "6.4.0" dependencies = [ "concat-idents", "inotify", @@ -4567,7 +4569,7 @@ dependencies = [ [[package]] name = "rog_profiles" -version = "6.3.0" +version = "6.4.0" dependencies = [ "log", "rog_platform", @@ -4578,7 +4580,7 @@ dependencies = [ [[package]] name = "rog_scsi" -version = "6.3.0" +version = "6.4.0" dependencies = [ "ron", "serde", @@ -4588,7 +4590,7 @@ dependencies = [ [[package]] name = "rog_simulators" -version = "6.3.0" +version = "6.4.0" dependencies = [ "log", "rog_anime", @@ -4598,7 +4600,7 @@ dependencies = [ [[package]] name = "rog_slash" -version = "6.3.0" +version = "6.4.0" dependencies = [ "dmi_id", "serde", diff --git a/Cargo.toml b/Cargo.toml index ebd6c01f..46672d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "6.3.0" +version = "6.4.0" rust-version = "1.82" license = "MPL-2.0" readme = "README.md" diff --git a/asusd/Cargo.toml b/asusd/Cargo.toml index 2e12a2e9..00e6af7e 100644 --- a/asusd/Cargo.toml +++ b/asusd/Cargo.toml @@ -42,6 +42,7 @@ logind-zbus.workspace = true serde.workspace = true concat-idents.workspace = true +serde_json = "1.0.149" [dev-dependencies] cargo-husky.workspace = true 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)) diff --git a/rog-aura/src/animation.rs b/rog-aura/src/animation.rs new file mode 100644 index 00000000..078360b3 --- /dev/null +++ b/rog-aura/src/animation.rs @@ -0,0 +1,145 @@ +//! Software-controlled LED animation modes for the asusd daemon. +//! +//! These modes run as background tasks in asusd and continuously update +//! the LED colors without requiring the GUI to be open. + +use serde::{Deserialize, Serialize}; + +use crate::Colour; + +/// Animation modes that can be run by the asusd daemon. +/// +/// Note: This type is serialized as JSON for DBus transport since zvariant +/// doesn't support enums with heterogeneous variants. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum AnimationMode { + /// No animation running + None, + /// Rainbow effect - cycles through HSV hue (0-360°) + Rainbow { + /// Update interval in milliseconds (lower = faster) + speed_ms: u32, + }, + /// Color cycle - transitions between a list of colors + ColorCycle { + /// Update interval in milliseconds + speed_ms: u32, + /// Colors to cycle through + colors: Vec, + }, + /// Breathe effect - fades between two colors through black + Breathe { + /// Update interval in milliseconds + speed_ms: u32, + /// Primary color + color1: Colour, + /// Secondary color (fades to this, then back) + color2: Colour, + }, + /// Pulse effect - brightness animation (dims and brightens) + Pulse { + /// Update interval in milliseconds + speed_ms: u32, + /// Base color to pulse + color: Colour, + /// Minimum brightness factor (0.0 - 1.0) + min_brightness: f32, + /// Maximum brightness factor (0.0 - 1.0) + max_brightness: f32, + }, +} + +impl Default for AnimationMode { + fn default() -> Self { + Self::None + } +} + +impl AnimationMode { + /// Returns true if this is an active animation mode + pub fn is_active(&self) -> bool { + !matches!(self, Self::None) + } + + /// Get the speed/interval in milliseconds + pub fn speed_ms(&self) -> u32 { + match self { + Self::None => 0, + Self::Rainbow { speed_ms } => *speed_ms, + Self::ColorCycle { speed_ms, .. } => *speed_ms, + Self::Breathe { speed_ms, .. } => *speed_ms, + Self::Pulse { speed_ms, .. } => *speed_ms, + } + } +} + +/// Apply a brightness factor to a color by scaling RGB values. +/// This allows simulating granular brightness on hardware with limited levels. +pub fn apply_brightness(color: Colour, factor: f32) -> Colour { + let factor = factor.clamp(0.0, 1.0); + Colour { + r: (color.r as f32 * factor) as u8, + g: (color.g as f32 * factor) as u8, + b: (color.b as f32 * factor) as u8, + } +} + +/// Convert HSV to RGB color. +/// Hue: 0-360, Saturation: 0.0-1.0, Value: 0.0-1.0 +pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Colour { + let c = v * s; + let h_prime = h / 60.0; + let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs()); + let m = v - c; + + let (r1, g1, b1) = match h_prime as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + + Colour { + r: ((r1 + m) * 255.0) as u8, + g: ((g1 + m) * 255.0) as u8, + b: ((b1 + m) * 255.0) as u8, + } +} + +/// Linear interpolation between two colors +pub fn lerp_colour(c1: &Colour, c2: &Colour, t: f32) -> Colour { + let t = t.clamp(0.0, 1.0); + Colour { + r: (c1.r as f32 + (c2.r as f32 - c1.r as f32) * t) as u8, + g: (c1.g as f32 + (c2.g as f32 - c1.g as f32) * t) as u8, + b: (c1.b as f32 + (c2.b as f32 - c1.b as f32) * t) as u8, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hsv_to_rgb_red() { + let c = hsv_to_rgb(0.0, 1.0, 1.0); + assert_eq!(c.r, 255); + assert_eq!(c.g, 0); + assert_eq!(c.b, 0); + } + + #[test] + fn test_brightness() { + let c = Colour { + r: 100, + g: 200, + b: 50, + }; + let dim = apply_brightness(c, 0.5); + assert_eq!(dim.r, 50); + assert_eq!(dim.g, 100); + assert_eq!(dim.b, 25); + } +} diff --git a/rog-aura/src/lib.rs b/rog-aura/src/lib.rs index 6ff39133..6d45f2df 100644 --- a/rog-aura/src/lib.rs +++ b/rog-aura/src/lib.rs @@ -16,6 +16,10 @@ pub mod effects; mod builtin_modes; pub use builtin_modes::*; +/// Software-controlled animation modes for daemon +pub mod animation; +pub use animation::{apply_brightness, hsv_to_rgb, lerp_colour, AnimationMode}; + /// Helper for detecting what is available pub mod aura_detection; pub mod error; diff --git a/rog-control-center/Cargo.toml b/rog-control-center/Cargo.toml index 936d0db8..5c1613b9 100644 --- a/rog-control-center/Cargo.toml +++ b/rog-control-center/Cargo.toml @@ -47,6 +47,7 @@ concat-idents.workspace = true futures-util.workspace = true versions.workspace = true +serde_json = "1.0.149" [dependencies.slint] git = "https://github.com/slint-ui/slint.git" diff --git a/rog-control-center/src/mocking.rs b/rog-control-center/src/mocking.rs index 7059c170..5a124b79 100644 --- a/rog-control-center/src/mocking.rs +++ b/rog-control-center/src/mocking.rs @@ -8,7 +8,6 @@ use rog_platform::supported::{ PlatformProfileFunctions, RogBiosSupportedFunctions, SupportedFunctions, }; use rog_profiles::fan_curve_set::{CurveData, FanCurveSet}; -use supergfxctl::pci_device::{GfxMode, GfxPower}; use crate::error::Result; diff --git a/rog-control-center/src/notify.rs b/rog-control-center/src/notify.rs index 1ec74460..d529624b 100644 --- a/rog-control-center/src/notify.rs +++ b/rog-control-center/src/notify.rs @@ -1,4 +1,4 @@ -//! `update_and_notify` is responsible for both notifications *and* updating +//! update_and_notify is responsible for both notifications and updating //! stored statuses about the system state. This is done through either direct, //! intoify, zbus notifications or similar methods. //! @@ -15,7 +15,6 @@ use rog_dbus::zbus_platform::PlatformProxy; use rog_platform::platform::PlatformProfile; use rog_platform::power::AsusPower; use serde::{Deserialize, Serialize}; -use supergfxctl::pci_device::GfxPower; use tokio::runtime::Runtime; use tokio::task::JoinHandle; @@ -28,8 +27,6 @@ const NOTIF_HEADER: &str = "ROG Control"; #[serde(default)] pub struct EnabledNotifications { pub enabled: bool, - pub receive_notify_gfx: bool, - pub receive_notify_gfx_status: bool, pub receive_notify_platform_profile: bool, } @@ -37,59 +34,11 @@ impl Default for EnabledNotifications { fn default() -> Self { Self { enabled: true, - receive_notify_gfx: true, - receive_notify_gfx_status: true, receive_notify_platform_profile: true, } } } -fn start_dpu_status_mon(config: Arc>) { - use supergfxctl::pci_device::Device; - let dev = Device::find().unwrap_or_default(); - let mut found_dgpu = false; // just for logging - for dev in dev { - if dev.is_dgpu() { - info!( - "Found dGPU: {}, starting status notifications", - dev.pci_id() - ); - let enabled_notifications_copy = config.clone(); - // Plain old thread is perfectly fine since most of this is potentially blocking - std::thread::spawn(move || { - let mut last_status = GfxPower::Unknown; - loop { - std::thread::sleep(Duration::from_millis(1500)); - if let Ok(status) = dev.get_runtime_status() { - if status != GfxPower::Unknown && status != last_status { - if let Ok(config) = enabled_notifications_copy.lock() { - if !config.notifications.receive_notify_gfx_status - || !config.notifications.enabled - { - continue; - } - } - // Required check because status cycles through - // active/unknown/suspended - do_gpu_status_notif("dGPU status changed:", &status) - .show() - .unwrap() - .on_close(|_| ()); - debug!("dGPU status changed: {:?}", &status); - } - last_status = status; - } - } - }); - found_dgpu = true; - break; - } - } - if !found_dgpu { - warn!("Did not find a dGPU on this system, dGPU status won't be avilable"); - } -} - /// Start monitoring for platform profile changes (triggered by Fn+F5 or software) /// and display an OSD notification when the profile changes. fn start_platform_profile_mon(config: Arc>, rt: &Runtime) { @@ -196,49 +145,9 @@ pub fn start_notifications( } }); - info!("Attempting to start plain dgpu status monitor"); - start_dpu_status_mon(config.clone()); - info!("Starting platform profile change monitor"); start_platform_profile_mon(config.clone(), rt); - // GPU MUX Mode notif - // TODO: need to get armoury attrs and iter to find - // let enabled_notifications_copy = config.clone(); - // tokio::spawn(async move { - // let conn = zbus::Connection::system().await.map_err(|e| { - // error!("zbus signal: receive_notify_gpu_mux_mode: {e}"); - // e - // })?; - // let proxy = PlatformProxy::new(&conn).await.map_err(|e| { - // error!("zbus signal: receive_notify_gpu_mux_mode: {e}"); - // e - // })?; - - // let mut actual_mux_mode = GpuMode::Error; - // if let Ok(mode) = proxy.gpu_mux_mode().await { - // actual_mux_mode = GpuMode::from(mode); - // } - - // info!("Started zbus signal thread: receive_notify_gpu_mux_mode"); - // while let Some(e) = - // proxy.receive_gpu_mux_mode_changed().await.next().await { if let - // Ok(config) = enabled_notifications_copy.lock() { if - // !config.notifications.enabled || !config.notifications.receive_notify_gfx { - // continue; - // } - // } - // if let Ok(out) = e.get().await { - // let mode = GpuMode::from(out); - // if mode == actual_mux_mode { - // continue; - // } - // do_mux_notification("Reboot required. BIOS GPU MUX mode set to", - // &mode).ok(); } - // } - // Ok::<(), zbus::Error>(()) - // }); - Ok(vec![blocking]) } @@ -255,19 +164,6 @@ where notif } -fn do_gpu_status_notif(message: &str, data: &GfxPower) -> Notification { - let mut notif = base_notification(message, &<&str>::from(data).to_owned()); - let icon = match data { - GfxPower::Suspended => "asus_notif_blue", - GfxPower::Off => "asus_notif_green", - GfxPower::AsusDisabled => "asus_notif_white", - GfxPower::AsusMuxDiscreet | GfxPower::Active => "asus_notif_red", - GfxPower::Unknown => "gpu-integrated", - }; - notif.icon(icon); - notif -} - /// Create a notification for platform profile (power mode) changes. /// Uses profile-specific icons and user-friendly names. fn do_platform_profile_notif(message: &str, profile: &PlatformProfile) -> Notification { diff --git a/rog-control-center/src/tray.rs b/rog-control-center/src/tray.rs index 8a127994..feab04fc 100644 --- a/rog-control-center/src/tray.rs +++ b/rog-control-center/src/tray.rs @@ -6,12 +6,9 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; -use ksni::{Handle, Icon, TrayMethods}; -use log::{info, warn}; +use ksni::{Icon, TrayMethods}; +use log::info; use rog_platform::platform::Properties; -use supergfxctl::pci_device::{Device, GfxMode, GfxPower}; -use supergfxctl::zbus_proxy::DaemonProxy as GfxProxy; -use versions::Versioning; use crate::config::Config; use crate::zbus_proxies::{AppState, ROGCCZbusProxyBlocking}; @@ -20,11 +17,8 @@ const TRAY_LABEL: &str = "ROG Control Center"; const TRAY_ICON_PATH: &str = "/usr/share/icons/hicolor/512x512/apps/"; struct Icons { - rog_blue: Icon, + #[allow(dead_code)] rog_red: Icon, - rog_green: Icon, - rog_white: Icon, - gpu_integrated: Icon, } static ICONS: OnceLock = OnceLock::new(); @@ -145,58 +139,6 @@ impl ksni::Tray for AsusTray { } } -async fn set_tray_icon_and_tip( - mode: GfxMode, - power: GfxPower, - tray: &mut Handle, - supergfx_active: bool, -) { - if let Some(icons) = ICONS.get() { - let icon = match power { - GfxPower::Suspended => icons.rog_blue.clone(), - GfxPower::Off => { - if mode == GfxMode::Vfio { - icons.rog_red.clone() - } else { - icons.rog_green.clone() - } - } - GfxPower::AsusDisabled => icons.rog_white.clone(), - GfxPower::AsusMuxDiscreet | GfxPower::Active => icons.rog_red.clone(), - GfxPower::Unknown => { - if supergfx_active { - icons.gpu_integrated.clone() - } else { - icons.rog_red.clone() - } - } - }; - - tray.update(|tray: &mut AsusTray| { - tray.current_icon = icon; - tray.current_title = format!( - "ROG: gpu mode = {mode:?}, gpu power = - {power:?}" - ); - }) - .await; - } -} - -fn find_dgpu() -> Option { - use supergfxctl::pci_device::Device; - let dev = Device::find().unwrap_or_default(); - for dev in dev { - if dev.is_dgpu() { - info!("Found dGPU: {}", dev.pci_id()); - // Plain old thread is perfectly fine since most of this is potentially blocking - return Some(dev); - } - } - warn!("Did not find a dGPU on this system, dGPU status won't be avilable"); - None -} - /// The tray is controlled somewhat by `Arc>` pub fn init_tray( _supported_properties: Vec, @@ -225,7 +167,7 @@ pub fn init_tray( }; // TODO: return an error to the UI - let mut tray; + let tray; match tray_init.disable_dbus_name(true).spawn().await { Ok(t) => tray = t, Err(e) => { @@ -237,57 +179,19 @@ pub fn init_tray( } info!("Tray started"); - let rog_blue = read_icon(&PathBuf::from("asus_notif_blue.png")); - let rog_green = read_icon(&PathBuf::from("asus_notif_green.png")); - let rog_white = read_icon(&PathBuf::from("asus_notif_white.png")); - let gpu_integrated = read_icon(&PathBuf::from("rog-control-center.png")); ICONS.get_or_init(|| Icons { - rog_blue, rog_red: rog_red.clone(), - rog_green, - rog_white, - gpu_integrated, }); - let mut has_supergfx = false; - let conn = zbus::Connection::system().await.unwrap(); - if let Ok(gfx_proxy) = GfxProxy::new(&conn).await { - match gfx_proxy.mode().await { - Ok(_) => { - has_supergfx = true; - if let Ok(version) = gfx_proxy.version().await { - if let Some(version) = Versioning::new(&version) { - let curr_gfx = Versioning::new("5.2.0").unwrap(); - warn!("supergfxd version = {version}"); - if version < curr_gfx { - // Don't allow mode changing if too old a version - warn!("supergfxd found but is too old to use"); - has_supergfx = false; - } - } - } - } - Err(e) => match e { - zbus::Error::MethodError(_, _, message) => { - warn!( - "Couldn't get mode from supergfxd: {message:?}, the supergfxd service \ - may not be running or installed" - ) - } - _ => warn!("Couldn't get mode from supergfxd: {e:?}"), - }, - } + info!("Started ROGTray"); - info!("Started ROGTray"); - let mut last_power = GfxPower::Unknown; - let dev = find_dgpu(); - - // Loop with select! to handle both periodic checks and stats updates - loop { - tokio::select! { - _ = stats_rx.changed() => { - let stats = stats_rx.borrow().clone(); - tray.update(move |t| { + loop { + tokio::select! { + _ = stats_rx.changed() => { + let stats = stats_rx.borrow().clone(); + let tray_update = tray.clone(); + tokio::spawn(async move { + tray_update.update(move |t| { t.cpu_temp = stats.cpu_temp; t.gpu_temp = stats.gpu_temp; t.cpu_fan = stats.cpu_fan; @@ -295,31 +199,12 @@ pub fn init_tray( t.power_w = stats.power_w; t.power_profile = stats.power_profile; }).await; - } - _ = tokio::time::sleep(Duration::from_millis(1000)) => { - if let Ok(lock) = config.try_lock() { - if !lock.enable_tray_icon { - return; - } - } - // Handle GPU icon updates - if has_supergfx { - if let Ok(mode) = gfx_proxy.mode().await { - if let Ok(power) = gfx_proxy.power().await { - if last_power != power { - set_tray_icon_and_tip(mode, power, &mut tray, has_supergfx).await; - last_power = power; - } - } - } - } else if let Some(dev) = dev.as_ref() { - if let Ok(power) = dev.get_runtime_status() { - if last_power != power { - set_tray_icon_and_tip(GfxMode::Hybrid, power, &mut tray, has_supergfx) - .await; - last_power = power; - } - } + }); + } + _ = tokio::time::sleep(Duration::from_millis(1000)) => { + if let Ok(lock) = config.try_lock() { + if !lock.enable_tray_icon { + return; } } } diff --git a/rog-control-center/src/ui/aura_animator.rs b/rog-control-center/src/ui/aura_animator.rs deleted file mode 100644 index e16b1a81..00000000 --- a/rog-control-center/src/ui/aura_animator.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Software-based keyboard animation for keyboards that only support Static mode. -//! Provides Rainbow and Color Cycle animations via timer-based color updates. - -use log::{info, warn}; -use slint::Weak; -use std::process::Command; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -use crate::MainWindow; - -/// Animation mode enum matching the UI -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum AnimationMode { - #[default] - None, - Rainbow, - ColorCycle, -} - -impl From for AnimationMode { - fn from(v: i32) -> Self { - match v { - 1 => AnimationMode::Rainbow, - 2 => AnimationMode::ColorCycle, - _ => AnimationMode::None, - } - } -} - -/// Shared state for the animator -pub struct AnimatorState { - /// Current animation mode - pub mode: AtomicU32, - /// Animation speed in milliseconds (update interval) - pub speed_ms: AtomicU32, - /// Stop signal - pub stop: AtomicBool, - /// Current hue for rainbow mode (0-360) - hue: AtomicU32, -} - -impl Default for AnimatorState { - fn default() -> Self { - Self { - mode: AtomicU32::new(0), - speed_ms: AtomicU32::new(200), - stop: AtomicBool::new(false), - hue: AtomicU32::new(0), - } - } -} - -/// Convert HSV to RGB (H: 0-360, S: 0-100, V: 0-100) -fn hsv_to_rgb(h: u32, s: u32, v: u32) -> (u8, u8, u8) { - let s = s as f32 / 100.0; - let v = v as f32 / 100.0; - let c = v * s; - let h_prime = (h as f32 / 60.0) % 6.0; - let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs()); - let m = v - c; - - let (r, g, b) = match h_prime as u32 { - 0 => (c, x, 0.0), - 1 => (x, c, 0.0), - 2 => (0.0, c, x), - 3 => (0.0, x, c), - 4 => (x, 0.0, c), - _ => (c, 0.0, x), - }; - - ( - ((r + m) * 255.0) as u8, - ((g + m) * 255.0) as u8, - ((b + m) * 255.0) as u8, - ) -} - -/// Format RGB as hex color string for asusctl -fn rgb_to_hex(r: u8, g: u8, b: u8) -> String { - format!("{:02x}{:02x}{:02x}", r, g, b) -} - -// Simple LCG for random numbers to avoid pulling in rand crate -fn next_random(seed: &mut u64) -> u32 { - *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); - (*seed >> 32) as u32 -} - -/// Start the animation loop (runs in tokio task) -pub fn start_animator(state: Arc, _ui_weak: Weak) { - info!("Starting keyboard animator"); - - tokio::spawn(async move { - // Local state for Color Cycle (RGB) - let mut current_r: f32 = 255.0; - let mut current_g: f32 = 0.0; - let mut current_b: f32 = 0.0; - let mut target_r: f32 = 0.0; - let mut target_g: f32 = 255.0; - let mut target_b: f32 = 0.0; - let mut seed = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(12345); - - loop { - // Check for stop signal - if state.stop.load(Ordering::Relaxed) { - info!("Animator stopping"); - break; - } - - let mode = AnimationMode::from(state.mode.load(Ordering::Relaxed) as i32); - // Cap speed at 150ms for stability - let raw_speed = state.speed_ms.load(Ordering::Relaxed); - let effective_speed = raw_speed.max(150) as u64; - - if mode == AnimationMode::None { - // No animation, sleep longer - tokio::time::sleep(Duration::from_millis(500)).await; - continue; - } - - // Calculate next color - let hex_color = match mode { - AnimationMode::Rainbow => { - // Hue step 1 for smooth, granular transitions - let hue = state.hue.fetch_add(1, Ordering::Relaxed) % 360; - let (r, g, b) = hsv_to_rgb(hue, 100, 100); - rgb_to_hex(r, g, b) - } - AnimationMode::ColorCycle => { - // RGB Linear Interpolation (Fading) - NOT Rainbow - - // 1. Check distance to target - let dist_sq = (target_r - current_r).powi(2) - + (target_g - current_g).powi(2) - + (target_b - current_b).powi(2); - - // If close, pick new random target color - if dist_sq < 100.0 { - let next_h = next_random(&mut seed) % 360; - let (r, g, b) = hsv_to_rgb(next_h, 100, 100); - target_r = r as f32; - target_g = g as f32; - target_b = b as f32; - } - - // 2. Lerp towards target (5% per frame for smooth ease-out) - let factor = 0.05; - current_r += (target_r - current_r) * factor; - current_g += (target_g - current_g) * factor; - current_b += (target_b - current_b) * factor; - - rgb_to_hex(current_r as u8, current_g as u8, current_b as u8) - } - AnimationMode::None => continue, - }; - - // Send color update via asusctl command (blocking, AWAITED to prevent races) - let hex = hex_color.clone(); - let _ = tokio::task::spawn_blocking(move || { - let result = Command::new("asusctl") - .args([ - "aura", "static", "-c", &hex, - ]) - .output(); - - if let Err(e) = result { - warn!("Failed to set aura color: {}", e); - } - }) - .await; - - // Sleep for the animation speed interval - tokio::time::sleep(Duration::from_millis(effective_speed)).await; - } - }); -} - -/// Stop the animator -pub fn stop_animator(state: &Arc) { - state.stop.store(true, Ordering::Relaxed); - state.mode.store(0, Ordering::Relaxed); -} - -/// Set animation mode -pub fn set_animation_mode(state: &Arc, mode: AnimationMode) { - state.mode.store(mode as u32, Ordering::Relaxed); - // Reset stop flag in case we're restarting - state.stop.store(false, Ordering::Relaxed); -} - -/// Set animation speed -pub fn set_animation_speed(state: &Arc, speed_ms: u32) { - let clamped = speed_ms.clamp(50, 2000); - state.speed_ms.store(clamped, Ordering::Relaxed); -} diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index 3adc544a..bf4d82b3 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -1,4 +1,3 @@ -pub mod aura_animator; pub mod setup_anime; pub mod setup_aura; pub mod setup_fan_curve_custom; @@ -6,7 +5,6 @@ pub mod setup_fans; pub mod setup_screenpad; pub mod setup_slash; pub mod setup_status; -pub mod setup_supergfx; pub mod setup_system; use std::sync::{Arc, Mutex}; @@ -24,7 +22,6 @@ use crate::ui::setup_fans::setup_fan_curve_page; use crate::ui::setup_screenpad::setup_screenpad; use crate::ui::setup_slash::setup_slash; use crate::ui::setup_status::setup_status; -use crate::ui::setup_supergfx::setup_supergfx; use crate::ui::setup_system::{setup_system_page, setup_system_page_callbacks}; use crate::{AppSettingsPageData, MainWindow}; @@ -116,20 +113,7 @@ pub fn setup_window( available.contains(&"xyz.ljones.Aura".to_string()), available.contains(&"xyz.ljones.Anime".to_string()), available.contains(&"xyz.ljones.Slash".to_string()), - // Supergfx check - { - if let Ok(conn) = zbus::blocking::Connection::system() { - zbus::blocking::fdo::DBusProxy::new(&conn) - .ok() - .and_then(|p| { - p.name_has_owner("org.supergfxctl.Daemon".try_into().ok()?) - .ok() - }) - .unwrap_or(false) - } else { - false - } - }, + false, // Screenpad check (Backlight interface) available.contains(&"xyz.ljones.Backlight".to_string()), available.contains(&"xyz.ljones.FanCurves".to_string()), @@ -186,9 +170,7 @@ pub fn setup_window( setup_slash(&ui, config.clone()); } - // Always try to setup supergfx if detected above, but for simplicity here we assume if sidebar has it (re-check or just run) // We didn't capture the boolean above. Let's just run it, it handles its own availability check internally via async proxy creation. - setup_supergfx(&ui, config.clone()); if available.contains(&"xyz.ljones.Backlight".to_string()) { setup_screenpad(&ui, config.clone()); @@ -237,22 +219,6 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc>) { }); // Granular notification toggles - let config_copy = config.clone(); - global.on_set_notify_gfx_switch(move |enable| { - if let Ok(mut lock) = config_copy.try_lock() { - lock.notifications.receive_notify_gfx = enable; - lock.write(); - } - }); - - let config_copy = config.clone(); - global.on_set_notify_gfx_status(move |enable| { - if let Ok(mut lock) = config_copy.try_lock() { - lock.notifications.receive_notify_gfx_status = enable; - lock.write(); - } - }); - let config_copy = config.clone(); global.on_set_notify_platform_profile(move |enable| { if let Ok(mut lock) = config_copy.try_lock() { @@ -267,8 +233,6 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc>) { global.set_startup_in_background(lock.startup_in_background); global.set_enable_tray_icon(lock.enable_tray_icon); global.set_notifications_enabled(lock.notifications.enabled); - global.set_notify_gfx_switch(lock.notifications.receive_notify_gfx); - global.set_notify_gfx_status(lock.notifications.receive_notify_gfx_status); global.set_notify_platform_profile(lock.notifications.receive_notify_platform_profile); } } diff --git a/rog-control-center/src/ui/setup_aura.rs b/rog-control-center/src/ui/setup_aura.rs index 5ba459f4..1e1466e3 100644 --- a/rog-control-center/src/ui/setup_aura.rs +++ b/rog-control-center/src/ui/setup_aura.rs @@ -1,15 +1,13 @@ use std::sync::{Arc, Mutex}; use log::{debug, error, info}; +use rog_aura::animation::AnimationMode; use rog_aura::keyboard::LaptopAuraPower; -use rog_aura::{AuraDeviceType, PowerZones}; +use rog_aura::{AuraDeviceType, Colour, PowerZones}; use rog_dbus::zbus_aura::AuraProxy; use slint::{ComponentHandle, Model, RgbaColor, SharedString}; use crate::config::Config; -use crate::ui::aura_animator::{ - set_animation_mode, set_animation_speed, start_animator, AnimationMode, AnimatorState, -}; use crate::ui::show_toast; use crate::{ set_ui_callbacks, set_ui_props_async, AuraPageData, MainWindow, PowerZones as SlintPowerZones, @@ -126,17 +124,15 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc>) { .ok(); } - // Create animator state (shared across callbacks) - let animator_state = Arc::new(AnimatorState::default()); - if let Ok(modes) = aura.supported_basic_modes().await { log::debug!("Available LED modes {modes:?}"); // Check if only Static mode is available (enable software animation) let static_only = modes.len() == 1 && modes.iter().any(|m| *m == 0.into()); - let handle_for_anim = handle.clone(); - let animator_state_clone = animator_state.clone(); + // Clone proxy for callbacks + let aura_for_animation = aura.clone(); + handle .upgrade_in_event_loop(move |handle| { let m: Vec = modes.iter().map(|n| (*n).into()).collect(); @@ -163,23 +159,69 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc>) { .global::() .set_soft_animation_available(true); - // Start the animator thread - start_animator(animator_state_clone.clone(), handle_for_anim.clone()); - - // Connect mode callback - let state_for_mode = animator_state_clone.clone(); + // Connect mode callback - uses DBus to start animation in daemon + let aura_mode = aura_for_animation.clone(); + let handle_weak = handle.as_weak(); handle .global::() .on_cb_soft_animation_mode(move |mode| { - set_animation_mode(&state_for_mode, AnimationMode::from(mode)); - }); + let aura_inner = aura_mode.clone(); + let handle = match handle_weak.upgrade() { + Some(h) => h, + None => return, + }; - // Connect speed callback - let state_for_speed = animator_state_clone.clone(); - handle - .global::() - .on_cb_soft_animation_speed(move |speed| { - set_animation_speed(&state_for_speed, speed as u32); + let data = handle.global::().get_led_mode_data(); + let c1 = data.colour1; + let c2 = data.colour2; + + let c1_rog = Colour { + r: c1.red(), + g: c1.green(), + b: c1.blue(), + }; + let c2_rog = Colour { + r: c2.red(), + g: c2.green(), + b: c2.blue(), + }; + + let anim_mode = match mode { + 1 => AnimationMode::Rainbow { speed_ms: 100 }, + 2 => AnimationMode::ColorCycle { + speed_ms: 200, + colors: vec![ + Colour { r: 255, g: 0, b: 0 }, + Colour { r: 0, g: 255, b: 0 }, + Colour { r: 0, g: 0, b: 255 }, + ], + }, + 3 => AnimationMode::Breathe { + speed_ms: 100, + color1: c1_rog, + color2: c2_rog, + }, + 4 => AnimationMode::Pulse { + speed_ms: 50, + color: c1_rog, + min_brightness: 0.2, + max_brightness: 1.0, + }, + _ => AnimationMode::None, + }; + tokio::spawn(async move { + let json = + serde_json::to_string(&anim_mode).unwrap_or_default(); + if anim_mode == AnimationMode::None { + if let Err(e) = aura_inner.stop_animation().await { + error!("Failed to stop animation: {e}"); + } + } else { + if let Err(e) = aura_inner.start_animation(json).await { + error!("Failed to start animation: {e}"); + } + } + }); }); } }) @@ -204,12 +246,79 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc>) { "Setting keyboard LEDmode failed" ); - set_ui_callbacks!(handle, - AuraPageData(.into()), - proxy_copy.led_mode_data(.into()), - "Keyboard LED mode set to {:?}", - "Setting keyboard LED mode failed" - ); + let proxy_data = proxy_copy.clone(); + let aura_soft = proxy_copy.clone(); + let handle_weak = handle.as_weak(); + + handle + .global::() + .on_cb_led_mode_data(move |data| { + // 1. Update hardware mode + let p = proxy_data.clone(); + let d = data.clone(); + tokio::spawn(async move { + if let Err(e) = p.set_led_mode_data(d.into()).await { + error!("Setting keyboard LED mode failed: {e}"); + } else { + debug!("Keyboard LED mode set"); + } + }); + + // 2. Update software animation if active + let handle = match handle_weak.upgrade() { + Some(h) => h, + None => return, + }; + + let soft_mode = handle.global::().get_soft_animation_mode(); + if soft_mode != 0 { + let c1 = data.colour1; + let c2 = data.colour2; + let c1_rog = Colour { + r: c1.red(), + g: c1.green(), + b: c1.blue(), + }; + let c2_rog = Colour { + r: c2.red(), + g: c2.green(), + b: c2.blue(), + }; + + let anim_mode = match soft_mode { + 1 => AnimationMode::Rainbow { speed_ms: 100 }, + 2 => AnimationMode::ColorCycle { + speed_ms: 200, + colors: vec![ + Colour { r: 255, g: 0, b: 0 }, + Colour { r: 0, g: 255, b: 0 }, + Colour { r: 0, g: 0, b: 255 }, + ], + }, + 3 => AnimationMode::Breathe { + speed_ms: 100, + color1: c1_rog, + color2: c2_rog, + }, + 4 => AnimationMode::Pulse { + speed_ms: 50, + color: c1_rog, + min_brightness: 0.2, + max_brightness: 1.0, + }, + _ => AnimationMode::None, + }; + + let aura_s = aura_soft.clone(); + tokio::spawn(async move { + if let Ok(json) = serde_json::to_string(&anim_mode) { + if let Err(e) = aura_s.start_animation(json).await { + error!("Failed to update software animation: {e}"); + } + } + }); + } + }); // set_ui_callbacks!(handle, // AuraPageData(.clone().into()), diff --git a/rog-control-center/src/ui/setup_supergfx.rs b/rog-control-center/src/ui/setup_supergfx.rs deleted file mode 100644 index 64995d4b..00000000 --- a/rog-control-center/src/ui/setup_supergfx.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::config::Config; -use crate::ui::show_toast; -use crate::{MainWindow, SupergfxPageData}; -use slint::{ComponentHandle, Model, SharedString, VecModel}; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; -use zbus::proxy; - -#[proxy( - interface = "org.supergfxctl.Daemon", - default_service = "org.supergfxctl.Daemon", - default_path = "/org/supergfxctl/Gfx" -)] -trait Supergfx { - fn supported(&self) -> zbus::Result>; - fn mode(&self) -> zbus::Result; - fn set_mode(&self, mode: &str) -> zbus::Result<()>; - fn vendor(&self) -> zbus::Result; -} - -pub fn setup_supergfx(ui: &MainWindow, _config: Arc>) { - let ui_weak = ui.as_weak(); - - tokio::spawn(async move { - let conn = match zbus::Connection::system().await { - Ok(c) => c, - Err(e) => { - log::warn!("Failed to connect to system bus: {}", e); - return; - } - }; - - let proxy = match SupergfxProxy::new(&conn).await { - Ok(p) => p, - Err(e) => { - log::warn!("Failed to create Supergfx proxy: {}", e); - return; - } - }; - - // Register Callbacks on UI Thread - { - let proxy_copy = proxy.clone(); - let ui_weak_copy = ui_weak.clone(); - let _ = ui_weak.upgrade_in_event_loop(move |ui| { - let handle_copy = ui_weak_copy.clone(); - ui.global::() - .on_set_mode(move |mode_str| { - let proxy = proxy_copy.clone(); - let handle = handle_copy.clone(); - tokio::spawn(async move { - show_toast( - format!("Switching to {}. Logout required.", mode_str).into(), - "Failed to set mode".into(), - handle, - proxy.set_mode(&mode_str).await, - ); - }); - }); - }); - } - - // Fetch Initial State - // Vendor - if let Ok(vendor) = proxy.vendor().await { - let _ = ui_weak.upgrade_in_event_loop(move |ui| { - ui.global::().set_vendor(vendor.into()) - }); - } - - // Supported Modes - if let Ok(supported) = proxy.supported().await { - let modes: Vec = supported - .iter() - .map(|s| SharedString::from(s.as_str())) - .collect(); - let _ = ui_weak.upgrade_in_event_loop(move |ui| { - let mode_model = Rc::new(VecModel::from(modes)); - ui.global::() - .set_supported_modes(mode_model.into()) - }); - } - - // Current Mode - if let Ok(mode) = proxy.mode().await { - let _ = ui_weak.upgrade_in_event_loop(move |ui| { - let g = ui.global::(); - g.set_current_mode(mode.clone().into()); - // Update selection index - let model = g.get_supported_modes(); - for (i, m) in model.iter().enumerate() { - if m == mode.as_str() { - g.set_selected_index(i as i32); - break; - } - } - }); - } - - // No signal monitoring implemented as supergfxctl state changes usually require user action/logout - }); -} diff --git a/rog-control-center/ui/pages/aura.slint b/rog-control-center/ui/pages/aura.slint index be7f4e6b..e188fd8f 100644 --- a/rog-control-center/ui/pages/aura.slint +++ b/rog-control-center/ui/pages/aura.slint @@ -1,9 +1,28 @@ -import { SystemDropdown, RogItem, SystemToggle, SystemToggleVert } from "../widgets/common.slint"; +import { + SystemDropdown, + RogItem, + SystemToggle, + SystemToggleVert, +} from "../widgets/common.slint"; import { Button, ComboBox, VerticalBox, GroupBox } from "std-widgets.slint"; import { RogPalette } from "../themes/rog_theme.slint"; -import { StyleMetrics, Slider, HorizontalBox, TextEdit, SpinBox, LineEdit, ScrollView } from "std-widgets.slint"; +import { + StyleMetrics, + Slider, + HorizontalBox, + TextEdit, + SpinBox, + LineEdit, + ScrollView, +} from "std-widgets.slint"; import { ColourSlider } from "../widgets/colour_picker.slint"; -import { AuraPageData, AuraDevType, PowerZones, LaptopAuraPower, AuraEffect } from "../types/aura_types.slint"; +import { + AuraPageData, + AuraDevType, + PowerZones, + LaptopAuraPower, + AuraEffect, +} from "../types/aura_types.slint"; import { AuraPowerGroup, AuraPowerGroupOld } from "../widgets/aura_power.slint"; export component PageAura inherits Rectangle { @@ -190,22 +209,21 @@ export component PageAura inherits Rectangle { VerticalLayout { padding: 10px; spacing: 8px; - Text { text: @tr("Software Animation (Static-only keyboards)"); font-size: 14px; font-weight: 600; color: RogPalette.accent; } - + HorizontalLayout { spacing: 20px; - VerticalLayout { Text { text: @tr("Animation Mode"); color: RogPalette.text-secondary; } + ComboBox { current_index <=> AuraPageData.soft_animation_mode; current_value: AuraPageData.soft_animation_modes[self.current-index]; @@ -215,22 +233,6 @@ export component PageAura inherits Rectangle { } } } - - VerticalLayout { - horizontal-stretch: 1; - Text { - text: @tr("Speed: ") + Math.round(AuraPageData.soft_animation_speed) + "ms"; - color: RogPalette.text-secondary; - } - Slider { - minimum: 150; - maximum: 1000; - value <=> AuraPageData.soft_animation_speed; - released => { - AuraPageData.cb_soft_animation_speed(Math.round(AuraPageData.soft_animation_speed)); - } - } - } } } } diff --git a/rog-control-center/ui/types/aura_types.slint b/rog-control-center/ui/types/aura_types.slint index 3bcb6e7e..16ba2f8d 100644 --- a/rog-control-center/ui/types/aura_types.slint +++ b/rog-control-center/ui/types/aura_types.slint @@ -179,6 +179,8 @@ export global AuraPageData { @tr("Animation mode" => "None"), @tr("Animation mode" => "Rainbow"), @tr("Animation mode" => "Color Cycle"), + @tr("Animation mode" => "Breathe"), + @tr("Animation mode" => "Pulse"), ]; in-out property soft_animation_mode: 0; in-out property soft_animation_speed: 200; // ms between updates diff --git a/rog-dbus/src/zbus_aura.rs b/rog-dbus/src/zbus_aura.rs index 95c97fb1..1c74f4db 100644 --- a/rog-dbus/src/zbus_aura.rs +++ b/rog-dbus/src/zbus_aura.rs @@ -84,6 +84,21 @@ pub trait Aura { /// SupportedPowerZones property #[zbus(property)] fn supported_power_zones(&self) -> zbus::Result>; + + /// Start a software-controlled animation in the daemon + /// `mode_json` is a JSON-serialized AnimationMode + fn start_animation(&self, mode_json: String) -> zbus::Result<()>; + + /// Stop any running animation + fn stop_animation(&self) -> zbus::Result<()>; + + /// AnimationRunning property - check if animation is active + #[zbus(property)] + fn animation_running(&self) -> zbus::Result; + + /// AnimationMode property - get current animation mode as JSON + #[zbus(property)] + fn animation_mode(&self) -> zbus::Result; } pub struct AuraProxyPerkey<'a>(AuraProxyBlocking<'a>);