From c8d7465ae752a4e242ed5e9603c752f66b7dae10 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:55:47 +0200 Subject: [PATCH 1/9] fix(asusctl): use slice::fill in anime-diag example Replace the manual element-wise loop with slice::fill to address the clippy::manual_slice_fill lint when mutating matrix rows. Signed-off-by: Marco Scardovi --- asusctl/examples/anime-diag.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/asusctl/examples/anime-diag.rs b/asusctl/examples/anime-diag.rs index 5302e61c3..2eb444bd7 100644 --- a/asusctl/examples/anime-diag.rs +++ b/asusctl/examples/anime-diag.rs @@ -24,9 +24,7 @@ fn main() { } for c in (0..35).step_by(step) { - for i in &mut matrix.get_mut()[c] { - *i = 50; - } + matrix.get_mut()[c].fill(50); } let anime_type = get_anime_type(); From c384aa965c7414c679ba0614a6c07f9b95417f1e Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:56:30 +0200 Subject: [PATCH 2/9] feat(rog-platform): add dynamic lighting led sysfs abstraction Introduce a validated wrapper for Linux Dynamic Lighting LED nodes. Require generic mandatory attributes, probe optional capabilities, and validate ranges, palette capacity, exact buffers, and sysfs errors. Map missing sysfs attributes on numeric writes to AttrNotFound. Expose optional enabled/enabled_index as true/false lighting-engine controls. Keep vendor-specific topology out of generic node validation. The unused optional frame sink remains deliberately unexposed. Signed-off-by: Marco Scardovi --- rog-platform/src/dynamic_led.rs | 284 ++++++++++++++++++++++++++++++++ rog-platform/src/lib.rs | 16 +- 2 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 rog-platform/src/dynamic_led.rs diff --git a/rog-platform/src/dynamic_led.rs b/rog-platform/src/dynamic_led.rs new file mode 100644 index 000000000..b7efe5249 --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,284 @@ +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +/// Dynamic Lighting class device under `/sys/class/leds/`. +/// +/// Wraps a kernel `led-class-dynamic` sysfs node exposing effects, palette, +/// speed, direction, power states, direct buffer streaming, and standard +/// brightness attributes. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct DynamicLed { + path: PathBuf, +} + +impl DynamicLed { + attr_string!("effect", path); + + attr_string!("effect_index", path); + + attr_string!("direction", path); + + attr_string!("direction_index", path); + + attr_string!("effects_palette", path); + + attr_string!("speed_range", path); + + attr_string!("zone_type", path); + + attr_string!("matrix_dimensions", path); + + attr_string!("power_states", path); + + attr_string!("power_states_index", path); + + attr_string!("enabled", path); + + attr_string!("enabled_index", path); + + attr_num!("speed", path, u32); + + attr_num!("max_palette_entries", path, u32); + + attr_num!("led_count", path, u32); + + attr_num!("brightness", path, u8); + + attr_num!("max_brightness", path, u8); + + /// Create a new `DynamicLed` by matching the exact sysfs name (e.g. + /// `"asus::keyboard"`). + pub fn new(name: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("DynamicLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("DynamicLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("DynamicLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname == name { + let syspath = device.syspath(); + if Self::is_dynamic_node(syspath) { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: syspath.to_path_buf(), + }); + } + } + } + + Err(PlatformError::MissingFunction(format!( + "DynamicLed::new(): no dynamic LED named '{name}' found" + ))) + } + + /// Helper to find a dynamic LED by name. + pub fn find(name: &str) -> Result { + Self::new(name) + } + + /// Return the LED name (e.g. "asus::keyboard"). + pub fn name(&self) -> &str { + self.path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + } + + fn is_dynamic_node(path: &Path) -> bool { + [ + "effect", "effect_index", "zone_type", "led_count", + ] + .iter() + .all(|attr| path.join(attr).exists()) + } + + /// Read and parse space-separated list of supported effects from + /// `effect_index`. + pub fn get_supported_effects_list(&self) -> Result> { + let raw = self.get_effect_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Check effect support while preserving sysfs read errors. + pub fn supports_effect(&self, effect: &str) -> Result { + Ok(self + .get_supported_effects_list()? + .iter() + .any(|candidate| candidate == effect)) + } + + /// Read and parse space-separated list of supported directions from + /// `direction_index`. + pub fn get_supported_directions_list(&self) -> Result> { + let raw = self.get_direction_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Lighting engine on/off from the optional `enabled` attribute. + pub fn is_lighting_enabled(&self) -> Result { + match self.get_enabled()?.trim() { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(PlatformError::InvalidValue), + } + } + + /// Set lighting engine on/off. Kernel values are `true` / `false`. + pub fn set_lighting_enabled(&self, enabled: bool) -> Result<()> { + if !self.has_enabled() { + return Err(PlatformError::AttrNotFound("enabled".into())); + } + self.set_enabled(if enabled { "true" } else { "false" }) + } + + /// Set speed after validating the optional advertised inclusive range. + pub fn set_supported_speed(&self, speed: u32) -> Result<()> { + if !self.has_speed() { + return Err(PlatformError::AttrNotFound("speed".into())); + } + if self.has_speed_range() { + let (min, max) = parse_speed_range(&self.get_speed_range()?)?; + if !(min..=max).contains(&speed) { + return Err(PlatformError::InvalidValue); + } + } + self.set_speed(speed) + } + + /// Set direction after validating the optional advertised values. + pub fn set_supported_direction(&self, direction: &str) -> Result<()> { + if !self.has_direction() { + return Err(PlatformError::AttrNotFound("direction".into())); + } + if self.has_direction_index() + && !self + .get_supported_directions_list()? + .iter() + .any(|candidate| candidate == direction) + { + return Err(PlatformError::InvalidValue); + } + self.set_direction(direction) + } + + /// Write one complete RGB frame to the `direct_buffer` binary attribute. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + let led_count = self.get_led_count()? as usize; + validate_direct_len(led_count, data.len())?; + + let direct_path = self.path.join("direct_buffer"); + let mut file = OpenOptions::new() + .write(true) + .open(&direct_path) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e))?; + file.write_all(data) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e)) + } + + /// Write palette colors as formatted `"#RRGGBB #RRGGBB ..."` string to + /// `effects_palette`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + if !self.has_effects_palette() { + return Err(PlatformError::AttrNotFound("effects_palette".into())); + } + if self.has_max_palette_entries() && colors.len() > self.get_max_palette_entries()? as usize + { + return Err(PlatformError::InvalidValue); + } + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + self.set_effects_palette(&palette_str) + } +} + +fn validate_direct_len(led_count: usize, data_len: usize) -> Result<()> { + let expected = led_count + .checked_mul(3) + .ok_or(PlatformError::InvalidValue)?; + if data_len == expected { + Ok(()) + } else { + Err(PlatformError::InvalidValue) + } +} + +fn parse_speed_range(raw: &str) -> Result<(u32, u32)> { + let values: Vec<_> = raw + .split(|c: char| !c.is_ascii_digit()) + .filter(|value| !value.is_empty()) + .map(str::parse::) + .collect::>() + .map_err(|_| PlatformError::ParseNum)?; + match values.as_slice() { + [min, max] if min <= max => Ok((*min, *max)), + _ => Err(PlatformError::InvalidValue), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + #[test] + fn test_palette_colors_formatting() { + let colors = [ + (255, 0, 128), + (0, 255, 64), + ]; + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + assert_eq!(palette_str, "#ff0080 #00ff40"); + } + + #[test] + fn validates_exact_direct_frame_size() { + assert!(super::validate_direct_len(4, 12).is_ok()); + assert!(super::validate_direct_len(4, 11).is_err()); + assert!(super::validate_direct_len(usize::MAX, 0).is_err()); + } + + #[test] + fn parses_bounded_speed_range() { + assert_eq!(super::parse_speed_range("0 2\n").unwrap(), (0, 2)); + assert_eq!(super::parse_speed_range("[1-4]").unwrap(), (1, 4)); + assert!(super::parse_speed_range("fast slow").is_err()); + assert!(super::parse_speed_range("4 1").is_err()); + } + + #[test] + fn validates_mandatory_generic_attributes() { + let path = + std::env::temp_dir().join(format!("asusctl-dynamic-led-test-{}", std::process::id())); + let _ = fs::remove_dir_all(&path); + fs::create_dir(&path).expect("temporary test directory must be creatable"); + for attr in [ + "effect", "effect_index", "zone_type", "led_count", + ] { + fs::write(path.join(attr), b"").expect("temporary attribute must be writable"); + } + assert!(super::DynamicLed::is_dynamic_node(&path)); + fs::remove_file(path.join("zone_type")).expect("test attribute must be removable"); + assert!(!super::DynamicLed::is_dynamic_node(&path)); + fs::remove_dir_all(path).expect("temporary test directory must be removable"); + } +} diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index dec0bb861..7a4650cd4 100644 --- a/rog-platform/src/lib.rs +++ b/rog-platform/src/lib.rs @@ -5,6 +5,7 @@ pub mod asus_armoury; pub mod backlight; pub mod cled; pub mod cpu; +pub mod dynamic_led; pub mod error; pub mod gpu_pci; pub mod hid_raw; @@ -16,6 +17,7 @@ pub mod usb_raw; use std::path::Path; +pub use dynamic_led::DynamicLed; use error::{PlatformError, Result}; use log::warn; use platform::PlatformProfile; @@ -74,13 +76,15 @@ pub fn write_attr_num(device: &mut Device, attr_name: &str, value: T) -> Resu where T: std::fmt::Display, { - if device + device .set_attribute_value(attr_name, format!("{value}")) - .is_err() - { - return Err(PlatformError::AttrNotFound(attr_name.to_owned())); - } - Ok(()) + .map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + PlatformError::AttrNotFound(attr_name.to_owned()) + } else { + PlatformError::IoPath(attr_name.to_owned(), e) + } + }) } pub fn read_attr_u8_array(device: &Device, attr_name: &str) -> Result> { From 14afea6f4eabc55f548bcbda8931b801e99a29a8 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:56:37 +0200 Subject: [PATCH 3/9] feat(rog-aura): add dynamic lighting mode and palette conversions Add mapping functions between ROG Aura structures and Dynamic Lighting class values: - AuraModeNum::to_dynamic_effect_str - Speed::to_dynamic_speed - Direction::to_dynamic_direction_str - AuraEffect::to_dynamic_palette for extracting RGB palette tuples - LedBrightness::{to_scaled, from_scaled} for mapping Off/Low/Med/High onto Dynamic Lighting 0..=max_brightness sysfs ranges Effect names follow the hid-asus effect_index vocabulary: static, breathe, rainbow_cycle, rainbow_wave, star, rain, highlight, laser, ripple, pulse, comet, flash, and direct. direct is AuraModeNum::Direct. Signed-off-by: Marco Scardovi --- rog-aura/src/builtin_modes.rs | 204 +++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..dc2504982 100644 --- a/rog-aura/src/builtin_modes.rs +++ b/rog-aura/src/builtin_modes.rs @@ -40,6 +40,46 @@ impl LedBrightness { Self::High => Self::Med, } } + + /// Map the 4-step UI level onto a sysfs brightness value. + /// + /// When `max_brightness <= 3` the legacy 0..=3 mapping is kept. Otherwise + /// levels are scaled across `0..=max_brightness` (Off=0, High=max). + pub const fn to_scaled(self, max_brightness: u8) -> u8 { + if max_brightness <= 3 { + return self as u8; + } + match self { + Self::Off => 0, + Self::Low => max_brightness / 3, + Self::Med => ((2u16 * max_brightness as u16) / 3) as u8, + Self::High => max_brightness, + } + } + + /// Inverse of [`Self::to_scaled`]. + pub const fn from_scaled(value: u8, max_brightness: u8) -> Self { + if max_brightness <= 3 { + return match value { + 0 => Self::Off, + 1 => Self::Low, + 3 => Self::High, + _ => Self::Med, + }; + } + if value == 0 { + return Self::Off; + } + let low = max_brightness / 3; + let med = ((2u16 * max_brightness as u16) / 3) as u8; + if value <= low { + Self::Low + } else if value <= med { + Self::Med + } else { + Self::High + } + } } impl From for LedBrightness { @@ -199,6 +239,16 @@ impl From for u8 { } } } + +impl Speed { + pub const fn to_dynamic_speed(&self) -> u32 { + match self { + Self::Low => 0, + Self::Med => 1, + Self::High => 2, + } + } +} /// Used for Rainbow mode. /// /// Enum corresponds to the required integer value @@ -248,6 +298,17 @@ impl From for i32 { } } +impl Direction { + pub const fn to_dynamic_direction_str(&self) -> &'static str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Up => "up", + Self::Down => "down", + } + } +} + /// Enum of modes that convert to the actual number required by a USB HID packet #[cfg_attr( feature = "dbus", @@ -271,6 +332,8 @@ pub enum AuraModeNum { Pulse = 10, Comet = 11, Flash = 12, + /// Kernel `direct` effect: per-key frames go through `direct_buffer`. + Direct = 13, } impl Display for AuraModeNum { @@ -300,6 +363,7 @@ impl From<&AuraModeNum> for &str { AuraModeNum::Pulse => "Pulse", AuraModeNum::Comet => "Comet", AuraModeNum::Flash => "Flash", + AuraModeNum::Direct => "Direct", } } } @@ -317,6 +381,7 @@ impl From<&str> for AuraModeNum { "Pulse" => AuraModeNum::Pulse, "Comet" => AuraModeNum::Comet, "Flash" => AuraModeNum::Flash, + "Direct" => AuraModeNum::Direct, _ => AuraModeNum::Static, } } @@ -336,6 +401,7 @@ impl From for AuraModeNum { 10 => AuraModeNum::Pulse, 11 => AuraModeNum::Comet, 12 => AuraModeNum::Flash, + 13 => AuraModeNum::Direct, _ => AuraModeNum::Static, } } @@ -359,6 +425,51 @@ impl From for AuraModeNum { } } +impl AuraModeNum { + /// Every mode the UI can offer. Index 9 is unused and is not included. + pub const fn all() -> [Self; 13] { + [ + Self::Static, + Self::Breathe, + Self::RainbowCycle, + Self::RainbowWave, + Self::Star, + Self::Rain, + Self::Highlight, + Self::Laser, + Self::Ripple, + Self::Pulse, + Self::Comet, + Self::Flash, + Self::Direct, + ] + } + + /// Dynamic Lighting `effect` name for this mode. + /// + /// Names match the hid-asus firmware effect table advertised in + /// `effect_index` (`static`, `breathe`, `rainbow_cycle`, `rainbow_wave`, + /// `star`, `rain`, `highlight`, `laser`, `ripple`, `pulse`, `comet`, + /// `flash`, plus `direct` when packed RGB is available). + pub const fn to_dynamic_effect_str(&self) -> Option<&'static str> { + match self { + Self::Static => Some("static"), + Self::Breathe => Some("breathe"), + Self::RainbowCycle => Some("rainbow_cycle"), + Self::RainbowWave => Some("rainbow_wave"), + Self::Star => Some("star"), + Self::Rain => Some("rain"), + Self::Highlight => Some("highlight"), + Self::Laser => Some("laser"), + Self::Ripple => Some("ripple"), + Self::Pulse => Some("pulse"), + Self::Comet => Some("comet"), + Self::Flash => Some("flash"), + Self::Direct => Some("direct"), + } + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +515,7 @@ impl FromStr for AuraZone { "3" | "three" => Ok(AuraZone::Key3), "4" | "four" => Ok(AuraZone::Key4), "5" | "logo" => Ok(AuraZone::Logo), - "6" | "lightbar-left" => Ok(AuraZone::BarLeft), + "6" | "lightbar-left" | "lightbar" | "bar" => Ok(AuraZone::BarLeft), "7" | "lightbar-right" => Ok(AuraZone::BarRight), _ => Err(Error::ParseSpeed), } @@ -469,6 +580,16 @@ impl AuraEffect { pub fn zone(&self) -> AuraZone { self.zone } + + /// Convert the effect colours to an array of RGB tuples for Dynamic Lighting palette. + pub fn to_dynamic_palette(&self) -> Vec<(u8, u8, u8)> { + let mut p = Vec::with_capacity(2); + p.push((self.colour1.r, self.colour1.g, self.colour1.b)); + if self.colour2.r != 0 || self.colour2.g != 0 || self.colour2.b != 0 { + p.push((self.colour2.r, self.colour2.g, self.colour2.b)); + } + p + } } impl Default for AuraEffect { @@ -539,9 +660,28 @@ impl From<&AuraEffect> for Vec { #[cfg(test)] mod tests { use crate::{ - AURA_LAPTOP_LED_MSG_LEN, AuraEffect, AuraModeNum, AuraZone, Colour, Direction, Speed, + AURA_LAPTOP_LED_MSG_LEN, AuraEffect, AuraModeNum, AuraZone, Colour, Direction, + LedBrightness, Speed, }; + #[test] + fn led_brightness_scales_for_dynamic_lighting() { + assert_eq!(LedBrightness::Off.to_scaled(255), 0); + assert_eq!(LedBrightness::Low.to_scaled(255), 85); + assert_eq!(LedBrightness::Med.to_scaled(255), 170); + assert_eq!(LedBrightness::High.to_scaled(255), 255); + + assert_eq!(LedBrightness::from_scaled(0, 255), LedBrightness::Off); + assert_eq!(LedBrightness::from_scaled(85, 255), LedBrightness::Low); + assert_eq!(LedBrightness::from_scaled(170, 255), LedBrightness::Med); + assert_eq!(LedBrightness::from_scaled(255, 255), LedBrightness::High); + + // Legacy 0..=3 path when max_brightness is small. + assert_eq!(LedBrightness::Med.to_scaled(3), 2); + assert_eq!(LedBrightness::from_scaled(2, 3), LedBrightness::Med); + assert_eq!(LedBrightness::High.to_scaled(3), 3); + } + #[test] fn check_led_static_packet() { let st = AuraEffect { @@ -689,4 +829,64 @@ mod tests { capture[..9] ); } + + #[test] + fn test_dynamic_lighting_conversions() { + assert_eq!(AuraModeNum::Static.to_dynamic_effect_str(), Some("static")); + assert_eq!( + AuraModeNum::Breathe.to_dynamic_effect_str(), + Some("breathe") + ); + assert_eq!( + AuraModeNum::RainbowCycle.to_dynamic_effect_str(), + Some("rainbow_cycle") + ); + assert_eq!( + AuraModeNum::RainbowWave.to_dynamic_effect_str(), + Some("rainbow_wave") + ); + assert_eq!(AuraModeNum::Pulse.to_dynamic_effect_str(), Some("pulse")); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), Some("flash")); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), Some("star")); + assert_eq!(AuraModeNum::Rain.to_dynamic_effect_str(), Some("rain")); + assert_eq!( + AuraModeNum::Highlight.to_dynamic_effect_str(), + Some("highlight") + ); + assert_eq!(AuraModeNum::Laser.to_dynamic_effect_str(), Some("laser")); + assert_eq!(AuraModeNum::Ripple.to_dynamic_effect_str(), Some("ripple")); + assert_eq!(AuraModeNum::Comet.to_dynamic_effect_str(), Some("comet")); + assert_eq!(AuraModeNum::Direct.to_dynamic_effect_str(), Some("direct")); + + assert_eq!(Speed::Low.to_dynamic_speed(), 0); + assert_eq!(Speed::Med.to_dynamic_speed(), 1); + assert_eq!(Speed::High.to_dynamic_speed(), 2); + + assert_eq!(Direction::Right.to_dynamic_direction_str(), "right"); + assert_eq!(Direction::Left.to_dynamic_direction_str(), "left"); + assert_eq!(Direction::Up.to_dynamic_direction_str(), "up"); + assert_eq!(Direction::Down.to_dynamic_direction_str(), "down"); + + let effect = AuraEffect { + colour1: Colour { + r: 0xff, + g: 0x10, + b: 0x20, + }, + colour2: Colour { + r: 0x00, + g: 0x30, + b: 0x40, + }, + ..Default::default() + }; + let palette = effect.to_dynamic_palette(); + assert_eq!( + palette, + vec![ + (0xff, 0x10, 0x20), + (0x00, 0x30, 0x40) + ] + ); + } } From 9567674cc2d57abbf82eccc08ae3f4121ea10d6d Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:35:15 +0200 Subject: [PATCH 4/9] feat(asusd): prefer dynamic lighting with hidraw fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prioritize valid Dynamic Lighting sysfs nodes for laptop Aura control and keep hidraw only as a capability-based fallback when no DL node exists. The two paths stay mutually exclusive for one device. Drive independent aura:keyboard and aura:lightbar nodes, map lighting off through enabled=false, stream exact led_count*3 RGB payloads through direct_buffer, map legacy Key1–4 / bar zones onto keyboard/lightbar, and scale Fn-key brightness across writable DL nodes. Under Dynamic Lighting, supported_basic_modes is the full Aura catalog intersected with the kernel effect_index. Document the userspace DL vs hidraw policy. Signed-off-by: Marco Scardovi --- asusd/src/aura_anime/mod.rs | 19 +- asusd/src/aura_laptop/mod.rs | 635 +++++++++++++++++++++++---- asusd/src/aura_laptop/trait_impls.rs | 168 ++++--- asusd/src/aura_manager.rs | 72 +-- asusd/src/aura_types.rs | 69 +-- docs/SUMMARY.md | 1 + docs/usage/dynamic-lighting.md | 60 +++ rog-platform/src/hid_raw.rs | 81 +++- 8 files changed, 826 insertions(+), 279 deletions(-) create mode 100644 docs/usage/dynamic-lighting.md diff --git a/asusd/src/aura_anime/mod.rs b/asusd/src/aura_anime/mod.rs index 5d622f3ab..0d3f6230b 100644 --- a/asusd/src/aura_anime/mod.rs +++ b/asusd/src/aura_anime/mod.rs @@ -9,12 +9,12 @@ use std::thread::sleep; use config_traits::StdConfig; use log::{debug, error, info, warn}; +use rog_anime::error::AnimeError; use rog_anime::usb::{ Brightness, pkt_flush, pkt_set_brightness, pkt_set_enable_display, pkt_set_enable_powersave_anim, pkts_for_init, }; use rog_anime::{ActionData, AnimeDataBuffer, AnimePacketType}; -use rog_platform::hid_raw::HidRaw; use rog_platform::usb_raw::USBRaw; use tokio::sync::Mutex; @@ -23,7 +23,6 @@ use crate::error::RogError; #[derive(Debug, Clone)] pub struct AniMe { - hid: Option>>, usb: Option>>, config: Arc>, cache: AniMeConfigCached, @@ -34,13 +33,8 @@ pub struct AniMe { } impl AniMe { - pub fn new( - hid: Option>>, - usb: Option>>, - config: Arc>, - ) -> Self { + pub fn new(usb: Option>>, config: Arc>) -> Self { Self { - hid, usb, config, cache: AniMeConfigCached::default(), @@ -78,11 +72,10 @@ impl AniMe { } pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; - } + let Some(usb) = &self.usb else { + return Err(RogError::Anime(AnimeError::NoDevice)); + }; + usb.lock().await.write_bytes(message)?; Ok(()) } diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 390ab27c8..e42475d58 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -2,10 +2,12 @@ use std::sync::Arc; use config::AuraConfig; use config_traits::StdConfig; -use log::info; -use rog_aura::keyboard::{AuraLaptopUsbPackets, LedUsbPackets}; +use log::{debug, info}; +use rog_aura::keyboard::{AuraLaptopUsbPackets, AuraPowerState, LedUsbPackets}; use rog_aura::usb::{AURA_LAPTOP_LED_APPLY, AURA_LAPTOP_LED_SET}; use rog_aura::{AURA_LAPTOP_LED_MSG_LEN, AuraDeviceType, AuraEffect, LedBrightness, PowerZones}; +use rog_platform::DynamicLed; +use rog_platform::error::PlatformError; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -17,12 +19,55 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Aura { + pub dynamic_kbd: Option>>, + pub dynamic_lightbar: Option>>, pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, } impl Aura { + #[must_use] + pub fn has_dynamic_lighting(&self) -> bool { + self.dynamic_kbd.is_some() || self.dynamic_lightbar.is_some() + } + + /// Keyboard then lightbar. Each node is independently writable. + fn dynamic_nodes(&self) -> [Option<&Arc>>; 2] { + [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + ] + } + + /// Map a D-Bus / config power zone onto the matching Dynamic Lighting nodes. + fn dynamic_nodes_for_power_zone(&self, zone: PowerZones) -> Vec<&Arc>> { + match zone { + PowerZones::Lightbar => self.dynamic_lightbar.iter().collect(), + PowerZones::KeyboardAndLightbar | PowerZones::None => { + self.dynamic_nodes().into_iter().flatten().collect() + } + // Logo tracks keyboard power in hid-asus; other chassis zones map + // to the keyboard DL node when present. + PowerZones::Keyboard + | PowerZones::Logo + | PowerZones::Lid + | PowerZones::RearGlow + | PowerZones::Ally => self.dynamic_kbd.iter().collect(), + } + } + + /// Prefer the keyboard `effect_index`; fall back to lightbar. + pub(crate) async fn effect_source(&self) -> Option> { + if let Some(kbd) = &self.dynamic_kbd { + Some(kbd.lock().await) + } else if let Some(lb) = &self.dynamic_lightbar { + Some(lb.lock().await) + } else { + None + } + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { Ok(()) @@ -35,14 +80,11 @@ impl Aura { /// Will lock the internal config and update. If anything else has locked /// this in scope then a deadlock can occur. pub async fn update_config(&self) -> Result<(), RogError> { + let bright = self.get_brightness().await; let mut config = self.config.lock().await; - let bright = if let Some(bl) = self.backlight.as_ref() { - bl.lock().await.get_brightness().unwrap_or_default() - } else { - config.brightness.into() - }; + let bright = bright.unwrap_or(config.brightness); config.read(); - config.brightness = bright.into(); + config.brightness = bright; config.write(); Ok(()) } @@ -93,80 +135,261 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { - if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { - if let Some(platform) = &self.backlight { - platform - .lock() - .await - .set_kbd_rgb_mode(&tuf_kbd_rgb_mode_bytes(mode))?; + // Priority: Dynamic Lighting sysfs interface. + // Key1–4 map to the whole keyboard node; BarLeft/BarRight to lightbar. + // Legacy Logo / other zones remain unsupported under DL. + if self.has_dynamic_lighting() && !supports_dynamic_zone(mode.zone) { + return Err(RogError::MissingFunction( + "Dynamic Lighting exposes keyboard/lightbar topology, not legacy Aura subzones" + .into(), + )); + } + if self.has_dynamic_lighting() + && let Some(eff_str) = mode.mode.to_dynamic_effect_str() + { + let speed = mode.speed.to_dynamic_speed(); + let dir_str = mode.direction.to_dynamic_direction_str(); + let palette = mode.to_dynamic_palette(); + + let apply_to_led = |led: &DynamicLed| -> Result { + if !led.supports_effect(eff_str)? { + return Ok(false); + } + // Firmware effects do not commit while lighting is off. + if led.has_enabled() && map_dl_result(led.set_lighting_enabled(true))?.is_none() { + return Ok(false); + } + if led.has_speed() && map_dl_result(led.set_supported_speed(speed))?.is_none() { + return Ok(false); + } + if led.has_direction() + && map_dl_result(led.set_supported_direction(dir_str))?.is_none() + { + return Ok(false); + } + if led.has_effects_palette() + && !palette.is_empty() + && map_dl_result(led.set_palette_colors(&palette))?.is_none() + { + return Ok(false); + } + Ok(map_dl_result(led.set_effect(eff_str))?.is_some()) + }; + + match mode.zone { + rog_aura::AuraZone::BarLeft | rog_aura::AuraZone::BarRight => { + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led)? { + return Ok(()); + } + } + } + rog_aura::AuraZone::None => { + // Fan out to every chassis zone so both get the same effect. + let mut any = false; + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + any |= apply_to_led(&kbd_led)?; + } + if let Some(lb) = &self.dynamic_lightbar { + let lb_led = lb.lock().await; + any |= apply_to_led(&lb_led)?; + } + if any { + return Ok(()); + } + } + // Key1–4: whole keyboard node under DL (no legacy 4-zone split). + _ => { + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + if apply_to_led(&kbd_led)? { + return Ok(()); + } + } + } } + } + + // When Dynamic Lighting is active, do not fall back to raw hidraw or TUF + // platform + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "Dynamic lighting mode or zone not supported by kernel".to_string(), + )); + } + + // Fallback is selected only when no valid Dynamic Lighting node exists. + if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) + && let Some(platform) = &self.backlight + { + platform + .lock() + .await + .set_kbd_rgb_mode(&tuf_kbd_rgb_mode_bytes(mode))?; + return Ok(()); } else if let Some(hid_raw) = &self.hid { - // Some keyboard controllers (e.g. G533QS firmware) silently drop - // short HID writes and only honour packets matching the OUTPUT - // report size declared in the HID descriptor (64 bytes for the - // 0x5d report). Pad effect/SET/APPLY here so we keep working on - // newer Strix/Zephyrus models without regressing older laptops. - const PADDED_LEN: usize = 64; let bytes: [u8; AURA_LAPTOP_LED_MSG_LEN] = mode.into(); - let mut effect_padded = [0u8; PADDED_LEN]; - effect_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&bytes); - let mut set_padded = [0u8; PADDED_LEN]; - set_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_SET); - let mut apply_padded = [0u8; PADDED_LEN]; - apply_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_APPLY); let hid_raw = hid_raw.lock().await; - hid_raw.write_bytes(&effect_padded)?; - hid_raw.write_bytes(&set_padded)?; - // Changes won't persist unless apply is set - hid_raw.write_bytes(&apply_padded)?; - } else { - return Err(RogError::NoAuraKeyboard); + hid_raw.write_bytes(&pad_hid_report(&bytes))?; + hid_raw.write_bytes(&pad_hid_report(&AURA_LAPTOP_LED_SET))?; + hid_raw.write_bytes(&pad_hid_report(&AURA_LAPTOP_LED_APPLY))?; + return Ok(()); } - Ok(()) + Err(RogError::NoAuraKeyboard) } - pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + /// Read the current brightness as a 4-step [`LedBrightness`] level. + /// + /// Prefer the keyboard node. `enabled=false` is lighting-off and maps to + /// [`LedBrightness::Off`]; `brightness` alone is intensity and must not be + /// treated as off. Dynamic Lighting nodes may advertise `max_brightness > 3` + /// and are scaled back to Off/Low/Med/High. + pub async fn get_brightness(&self) -> Result { + for slot in self.dynamic_nodes().into_iter().flatten() { + let led = slot.lock().await; + if led.has_enabled() { + let Some(on) = map_dl_result(led.is_lighting_enabled())? else { + continue; + }; + if !on { + return Ok(LedBrightness::Off); + } + } + let Some(max) = map_dl_result(led.get_max_brightness())? else { + continue; + }; + let Some(value) = map_dl_result(led.get_brightness())? else { + continue; + }; + return Ok(LedBrightness::from_scaled(value, max)); + } if let Some(backlight) = &self.backlight { - backlight.lock().await.set_brightness(value)?; - return Ok(()); + let value = backlight.lock().await.get_brightness()?; + return Ok(value.into()); } Err(RogError::MissingFunction( "No LED backlight control available".to_string(), )) } + /// Set keyboard/Aura brightness from a 4-step [`LedBrightness`] level. + /// + /// [`LedBrightness::Off`] uses `enabled=false` when the node advertises it. + /// Other levels enable the lighting engine and scale intensity to each + /// node's `max_brightness`. The legacy `KeyboardBacklight` (0..=3) is also + /// updated when present. + pub async fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + let mut applied = false; + let off = brightness == LedBrightness::Off; + for led in self.dynamic_nodes().into_iter().flatten() { + let led = led.lock().await; + if led.has_enabled() { + if map_dl_result(led.set_lighting_enabled(!off))?.is_some() { + applied = true; + } + if off { + continue; + } + } + let max = led.get_max_brightness()?; + if map_dl_result(led.set_brightness(brightness.to_scaled(max)))?.is_some() { + applied = true; + } + } + + if let Some(backlight) = &self.backlight { + match backlight.lock().await.set_brightness(brightness.into()) { + Ok(()) => applied = true, + Err(e) if applied => { + debug!("WMI kbd backlight brightness sync failed: {e}"); + } + Err(e) => return Err(e.into()), + } + } + + if applied { + Ok(()) + } else { + Err(RogError::MissingFunction( + "No LED backlight control available".to_string(), + )) + } + } + /// Set combination state for boot animation/sleep animation/all leds/keys /// leds/side leds LED active pub async fn set_power_states(&self, config: &AuraConfig) -> Result<(), RogError> { - if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { - if let Some(backlight) = &self.backlight { - let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; + if self.has_dynamic_lighting() { + let mut applied = false; + let mut saw_power_states = false; + let mut unsupported_request = false; + // Apply each configured zone to its matching DL node(s). Do not OR + // every zone into one string shared by keyboard and lightbar. + for state in &config.enabled.states { + let requested = power_flags_for_state(state); + for led in self.dynamic_nodes_for_power_zone(state.zone) { + let led = led.lock().await; + if !led.has_power_states() { + continue; + } + saw_power_states = true; + let states = filter_power_states(&requested, &led.get_power_states_index()?); + if states.is_empty() && !requested.is_empty() { + unsupported_request = true; + continue; + } + if map_dl_result(led.set_power_states(&states.join(" ")))?.is_some() { + applied = true; + } + } + } + if applied { + return Ok(()); } + if !saw_power_states { + return Err(RogError::MissingFunction( + "Dynamic Lighting node does not expose power_states".into(), + )); + } + if unsupported_request { + return Err(RogError::MissingFunction( + "No requested power state is supported by this Dynamic Lighting node".into(), + )); + } + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted power_states".into(), + )); + } + + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && let Some(backlight) = &self.backlight + { + // TODO: tuf bool array + let buf = config.enabled.to_bytes(config.led_type); + backlight.lock().await.set_kbd_rgb_state(&buf)?; } else if let Some(hid_raw) = &self.hid { let hid_raw = hid_raw.lock().await; if let Some(p) = config.enabled.states.first() && p.zone == PowerZones::Ally { - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xd1, 0x09, 0x01, p.new_to_byte() as u8, - 0x0, - 0x0, - ]; - hid_raw.write_bytes(&msg)?; + 0, + 0, + ])?; return Ok(()); } - let bytes = config.enabled.to_bytes(config.led_type); - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], - ]; - hid_raw.write_bytes(&msg)?; + ])?; } Ok(()) } @@ -184,60 +407,172 @@ impl Aura { config.write(); } - let pkt_type = effect[0][1]; - const PER_KEY_TYPE: u8 = 0xbc; + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && !self.has_dynamic_lighting() + && let Some(tuf) = &self.backlight + { + for row in effect.iter() { + let rgb = row.get(9..12).ok_or(PlatformError::InvalidValue)?; + let [r, g, b] = rgb else { + return Err(PlatformError::InvalidValue.into()); + }; + tuf.lock().await.set_kbd_rgb_mode(&[ + 0, 0, *r, *g, *b, 0, + ])?; + } + return Ok(()); + } + + for maybe_led in self.dynamic_nodes().into_iter().flatten() { + let dynamic = maybe_led.lock().await; + let led_count = dynamic.get_led_count()? as usize; + let rgb_buf = direct_rgb_payload(effect, led_count)?; + if rgb_buf.is_empty() { + continue; + } + // Kernel direct_buffer is meaningful only while effect=direct and + // the zone is enabled; otherwise firmware effects keep owning the + // controller and frames are ignored or overwritten. + if map_dl_result(prepare_direct_streaming(&dynamic))?.is_none() { + debug!( + "Dynamic lighting node '{}' busy preparing direct (-EBUSY), trying alternate", + dynamic.name() + ); + continue; + } + if map_dl_result(dynamic.write_direct(&rgb_buf))?.is_none() { + debug!( + "Dynamic lighting node '{}' busy (-EBUSY), trying alternate", + dynamic.name() + ); + continue; + } + config.per_key_mode_active = true; + return Ok(()); + } + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted the direct frame".into(), + )); + } if let Some(hid_raw) = &self.hid { + let first = effect.first().ok_or(PlatformError::InvalidValue)?; + let packet_type = *first.get(1).ok_or(PlatformError::InvalidValue)?; + if effect.iter().any(|row| { + row.first() != Some(&0x5d) + || row.len() > 64 + || (packet_type == 0xbc && row.len() != 64) + }) { + return Err(PlatformError::InvalidValue.into()); + } let hid_raw = hid_raw.lock().await; - if pkt_type != PER_KEY_TYPE { + if packet_type != 0xbc { config.per_key_mode_active = false; - hid_raw.write_bytes(&effect[0])?; + hid_raw.write_bytes(first)?; hid_raw.write_bytes(&AURA_LAPTOP_LED_SET)?; - // hid_raw.write_bytes(&LED_APPLY)?; } else { if !config.per_key_mode_active { - let init = LedUsbPackets::get_init_msg(); - hid_raw.write_bytes(&init)?; + hid_raw.write_bytes(&LedUsbPackets::get_init_msg())?; config.per_key_mode_active = true; } - for row in effect.iter() { + for row in effect { hid_raw.write_bytes(row)?; } } - } else if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) - && let Some(tuf) = &self.backlight - { - for row in effect.iter() { - let r = row[9]; - let g = row[10]; - let b = row[11]; - tuf.lock().await.set_kbd_rgb_mode(&[ - 0, 0, r, g, b, 0, - ])?; - } + return Ok(()); } - Ok(()) + + Err(RogError::NoAuraKeyboard) } pub async fn fix_ally_power(&mut self) -> Result<(), RogError> { - if self.config.lock().await.led_type == AuraDeviceType::Ally - && let Some(hid_raw) = &self.hid - { + let needs_fix = { + let config = self.config.lock().await; + config.led_type == AuraDeviceType::Ally && config.ally_fix.is_none() + }; + if needs_fix && let Some(hid_raw) = &self.hid { + hid_raw.lock().await.write_bytes(&[ + 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, + ])?; let mut config = self.config.lock().await; - if config.ally_fix.is_none() { - let msg = [ - 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, - ]; - hid_raw.lock().await.write_bytes(&msg)?; - info!("Reset Ally power settings to base"); - config.ally_fix = Some(true); - } + config.ally_fix = Some(true); config.write(); } Ok(()) } } +fn direct_rgb_payload( + packets: &AuraLaptopUsbPackets, + led_count: usize, +) -> Result, PlatformError> { + let expected = led_count + .checked_mul(3) + .ok_or(PlatformError::InvalidValue)?; + let first = packets.first().ok_or(PlatformError::InvalidValue)?; + let mut rgb = Vec::with_capacity(expected); + if packets.len() == 1 && led_count == 4 { + rgb.extend_from_slice(first.get(9..21).ok_or(PlatformError::InvalidValue)?); + } else { + for packet in packets { + if packet.get(1).copied() != Some(0xbc) { + return Err(PlatformError::InvalidValue); + } + let count = *packet.get(7).ok_or(PlatformError::InvalidValue)? as usize; + let payload_len = count.checked_mul(3).ok_or(PlatformError::InvalidValue)?; + let end = 9usize + .checked_add(payload_len) + .ok_or(PlatformError::InvalidValue)?; + rgb.extend_from_slice(packet.get(9..end).ok_or(PlatformError::InvalidValue)?); + } + } + if rgb.len() != expected { + return Err(PlatformError::InvalidValue); + } + Ok(rgb) +} + +pub(super) fn supports_dynamic_zone(zone: rog_aura::AuraZone) -> bool { + matches!( + zone, + rog_aura::AuraZone::None + | rog_aura::AuraZone::Key1 + | rog_aura::AuraZone::Key2 + | rog_aura::AuraZone::Key3 + | rog_aura::AuraZone::Key4 + | rog_aura::AuraZone::BarLeft + | rog_aura::AuraZone::BarRight + ) +} + +/// `Ok(None)` when the node is inactive (`-EBUSY`). Other errors propagate. +fn map_dl_result(result: Result) -> Result, RogError> { + match result { + Ok(value) => Ok(Some(value)), + Err(err) => { + let err = RogError::from(err); + if is_dl_node_inactive(&err) { + Ok(None) + } else { + Err(err) + } + } + } +} + +/// Kernel may return `-EBUSY` when a Dynamic Lighting node briefly rejects a +/// write (for example while another zone owns a shared controller path). +fn is_dl_node_inactive(err: &RogError) -> bool { + let io = match err { + RogError::Platform(PlatformError::IoPath(_, e)) => Some(e), + RogError::Platform(PlatformError::Io(e)) => Some(e), + RogError::Write(_, e) | RogError::Path(_, e) | RogError::Io(e) => Some(e), + _ => None, + }; + io.is_some_and(|e| e.kind() == std::io::ErrorKind::ResourceBusy || e.raw_os_error() == Some(16)) +} + /// Build the `cmd mode r g b speed` values for the asus-wmi `kbd_rgb_mode` /// sysfs attribute used by TUF laptops. /// @@ -253,4 +588,142 @@ fn tuf_kbd_rgb_mode_bytes(mode: &AuraEffect) -> [u8; 6] { mode.colour1.b, mode.speed.into(), ] -} \ No newline at end of file +} + +fn pad_hid_report(bytes: &[u8]) -> [u8; 64] { + let mut padded = [0u8; 64]; + if let Some(slot) = padded.get_mut(..bytes.len()) { + slot.copy_from_slice(bytes); + } + padded +} + +fn filter_power_states<'a>(requested: &[&'a str], supported: &str) -> Vec<&'a str> { + let supported: std::collections::HashSet<_> = supported.split_whitespace().collect(); + requested + .iter() + .copied() + .filter(|state| supported.contains(state)) + .collect() +} + +fn power_flags_for_state(state: &AuraPowerState) -> Vec<&'static str> { + let mut requested = Vec::new(); + if state.boot { + requested.push("boot"); + } + if state.awake { + requested.push("awake"); + } + if state.sleep { + requested.push("sleep"); + } + if state.shutdown { + requested.push("shutdown"); + } + requested +} + +/// Enable the zone and select kernel `direct` before streaming RGB frames. +fn prepare_direct_streaming(led: &DynamicLed) -> Result<(), PlatformError> { + if led.has_enabled() { + led.set_lighting_enabled(true)?; + } + if led.supports_effect("direct")? + && led.get_effect().ok().as_deref().map(str::trim) != Some("direct") + { + led.set_effect("direct")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::{Error, ErrorKind}; + + use rog_aura::keyboard::AuraPowerState; + use rog_aura::{AuraZone, PowerZones}; + use rog_platform::error::PlatformError; + + use super::{ + direct_rgb_payload, filter_power_states, is_dl_node_inactive, power_flags_for_state, + supports_dynamic_zone, + }; + use crate::error::RogError; + + #[test] + fn direct_payload_requires_exact_size() { + let mut packet = vec![0; 15]; + packet[1] = 0xbc; + packet[7] = 2; + packet[9..15].copy_from_slice(&[ + 1, 2, 3, 4, 5, 6, + ]); + assert_eq!( + direct_rgb_payload(&vec![packet.clone()], 2).unwrap().len(), + 6 + ); + assert!(direct_rgb_payload(&vec![packet], 3).is_err()); + assert!(direct_rgb_payload(&Vec::new(), 1).is_err()); + } + + #[test] + fn power_states_are_intersected_with_capabilities() { + assert_eq!( + filter_power_states( + &[ + "boot", "awake", "shutdown" + ], + "awake sleep" + ), + ["awake"] + ); + } + + #[test] + fn power_flags_follow_individual_zone_state() { + let kbd = AuraPowerState { + zone: PowerZones::Keyboard, + boot: true, + awake: false, + sleep: false, + shutdown: false, + }; + let lb = AuraPowerState { + zone: PowerZones::Lightbar, + boot: false, + awake: true, + sleep: true, + shutdown: false, + }; + assert_eq!(power_flags_for_state(&kbd), ["boot"]); + assert_eq!( + power_flags_for_state(&lb), + [ + "awake", "sleep" + ] + ); + } + + #[test] + fn dynamic_topology_allows_keyboard_and_lightbar_proxies() { + assert!(supports_dynamic_zone(AuraZone::None)); + assert!(supports_dynamic_zone(AuraZone::Key1)); + assert!(supports_dynamic_zone(AuraZone::BarLeft)); + assert!(!supports_dynamic_zone(AuraZone::Logo)); + } + + #[test] + fn inactive_dl_node_detects_ebusy() { + let err = RogError::Platform(PlatformError::IoPath( + "asus::keyboard/brightness".into(), + Error::new(ErrorKind::ResourceBusy, "Device or resource busy"), + )); + assert!(is_dl_node_inactive(&err)); + let errno = RogError::Platform(PlatformError::IoPath( + "asus::keyboard/direct_buffer".into(), + Error::from_raw_os_error(16), + )); + assert!(is_dl_node_inactive(&errno)); + } +} diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 90629044d..f8561d161 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -9,7 +9,7 @@ use zbus::object_server::SignalEmitter; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; -use super::Aura; +use super::{Aura, supports_dynamic_zone}; use crate::error::RogError; use crate::{CtrlTask, Reloadable}; @@ -27,11 +27,8 @@ impl AuraZbus { pub async fn start_tasks( mut self, connection: &Connection, - // _signal_ctx: SignalEmitter<'static>, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); - // let signal_ctx = signal_ctx.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -39,11 +36,11 @@ impl AuraZbus { .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - // TODO: skip this until we keep handles to tasks so they can be killed - // task.create_tasks(signal_ctx).await - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } @@ -61,25 +58,17 @@ impl AuraZbus { /// Return the current LED brightness #[zbus(property)] async fn brightness(&self) -> Result { - if let Some(bl) = self.0.backlight.as_ref() { - return Ok(bl.lock().await.get_brightness().map(|n| n.into())?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + Ok(self.0.get_brightness().await?) } - /// Set the keyboard brightness level (0-3) + /// Set the keyboard brightness level (Off/Low/Med/High) #[zbus(property)] async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { - if let Some(bl) = self.0.backlight.as_ref() { - let res = bl.lock().await.set_brightness(brightness.into()); - if res.is_ok() { - let mut config = self.0.config.lock().await; - config.brightness = brightness; - config.write(); - } - return Ok(res?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + self.0.set_brightness(brightness).await?; + let mut config = self.0.config.lock().await; + config.brightness = brightness; + config.write(); + Ok(()) } /// Total levels of brightness available @@ -97,17 +86,46 @@ impl AuraZbus { #[zbus(property)] async fn supported_basic_modes(&self) -> Result, ZbErr> { let config = self.0.config.lock().await; + if self.0.has_dynamic_lighting() + && let Some(led) = self.0.effect_source().await + { + let mut modes = Vec::new(); + for mode in AuraModeNum::all() { + if let Some(eff_str) = mode.to_dynamic_effect_str() + && led.supports_effect(eff_str)? + { + modes.push(mode); + } + } + return Ok(modes); + } Ok(config.builtins.keys().cloned().collect()) } #[zbus(property)] async fn supported_basic_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + // The kernel nodes split keyboard from lightbar, but do not model + // the historical four keyboard or left/right lightbar zones. + return Ok(Vec::new()); + } let config = self.0.config.lock().await; Ok(config.support_data.basic_zones.clone()) } #[zbus(property)] async fn supported_power_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + let has_power_states = self + .0 + .effect_source() + .await + .is_some_and(|led| led.has_power_states()); + if !has_power_states { + // Avoid advertising zones the UI cannot actually control. + return Ok(Vec::new()); + } + } let config = self.0.config.lock().await; Ok(config.support_data.power_zones.clone()) } @@ -116,8 +134,6 @@ impl AuraZbus { #[zbus(property)] async fn led_mode(&self) -> Result { // entirely possible to deadlock here, so use try instead of lock() - // let ctrl = self.0.lock().await; - // Ok(config.current_mode) if let Ok(config) = self.0.config.try_lock() { Ok(config.current_mode) } else { @@ -137,7 +153,7 @@ impl AuraZbus { if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if let Err(e) = self.0.set_brightness(config.brightness.into()).await { + if let Err(e) = self.0.set_brightness(config.brightness).await { log::warn!("Could not set keyboard backlight brightness: {e}"); } config.write(); @@ -166,10 +182,28 @@ impl AuraZbus { #[zbus(property)] async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> { let mut config = self.0.config.lock().await; - if !config.support_data.basic_modes.contains(&effect.mode) - || effect.zone != AuraZone::None - && !config.support_data.basic_zones.contains(&effect.zone) - { + let (is_mode_supported, is_zone_supported) = if self.0.has_dynamic_lighting() { + let mode_ok = if let Some(eff_str) = effect.mode.to_dynamic_effect_str() { + match self.0.effect_source().await { + Some(led) => led.supports_effect(eff_str)?, + None => false, + } + } else { + false + }; + let zone_ok = supports_dynamic_zone(effect.zone); + (mode_ok, zone_ok) + } else { + ( + config.support_data.basic_modes.contains(&effect.mode), + effect.zone == AuraZone::None + || config.support_data.basic_zones.contains(&effect.zone) + || (self.0.dynamic_lightbar.is_some() + && matches!(effect.zone, AuraZone::BarLeft | AuraZone::BarRight)), + ) + }; + + if !is_mode_supported || !is_zone_supported { return Err(ZbErr::NotSupported(format!( "The Aura effect is not supported: {effect:?}" ))); @@ -181,7 +215,7 @@ impl AuraZbus { if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if let Err(e) = self.0.set_brightness(config.brightness.into()).await { + if let Err(e) = self.0.set_brightness(config.brightness).await { log::warn!("Could not set keyboard backlight brightness: {e}"); } config.set_builtin(effect); @@ -250,35 +284,16 @@ impl CtrlTask for AuraZbus { async move { if !sleeping { info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner1.backlight { - backlight - .lock() - .await - .set_brightness(inner1.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + let brightness = inner1.config.lock().await.brightness; + if let Err(e) = inner1.set_brightness(brightness).await { + error!("CtrlKbdLedTask: {e}"); } let mut config = inner1.config.lock().await; - inner1 - .write_current_config_mode(&mut config) - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); - } else if sleeping { - inner1 - .update_config() - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if let Err(e) = inner1.write_current_config_mode(&mut config).await { + error!("CtrlKbdLedTask: {e}"); + } + } else if let Err(e) = inner1.update_config().await { + error!("CtrlKbdLedTask: {e}"); } } }, @@ -286,17 +301,9 @@ impl CtrlTask for AuraZbus { let inner3 = inner3.clone(); async move { info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner3.backlight { - // unwrap as we want to bomb out of the task - backlight - .lock() - .await - .set_brightness(inner3.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + let brightness = inner3.config.lock().await.brightness; + if let Err(e) = inner3.set_brightness(brightness).await { + error!("CtrlKbdLedTask: {e}"); } } }, @@ -311,27 +318,6 @@ impl CtrlTask for AuraZbus { ) .await; - // let ctrl2 = self.0.clone(); - // let ctrl = self.0.lock().await; - // if ctrl.led_node.has_brightness_control() { - // let watch = ctrl.led_node.monitor_brightness()?; - // tokio::spawn(async move { - // let mut buffer = [0; 32]; - // watch - // .into_event_stream(&mut buffer) - // .unwrap() - // .for_each(|_| async { - // if let Some(lock) = ctrl2.try_lock() { - // load_save(true, lock).unwrap(); // unwrap as we want - // // to - // // bomb out of the - // // task - // } - // }) - // .await; - // }); - // } - Ok(()) } } diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d809bd744..7cebf5caf 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -7,7 +7,6 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use dmi_id::DMIID; use log::{debug, error, info, warn}; use mio::{Events, Interest, Poll, Token}; use rog_platform::error::PlatformError; @@ -196,29 +195,6 @@ impl DeviceManager { }); } } - // ANIME MATRIX DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_anime_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::AniMe(anime) = dev_type.clone() - { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_anime()); - let ctrl = AniMeZbus::new(anime); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start AniMe tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } - } // AURA LAPTOP DEVICE if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) @@ -450,7 +426,7 @@ impl DeviceManager { if matches!(dev.device, DeviceHandle::AniMe(_)) { do_anime = false; } - if matches!(dev.device, DeviceHandle::Aura(_) | DeviceHandle::OldAura(_)) { + if matches!(dev.device, DeviceHandle::Aura(_)) { do_kb_backlight = false; } } @@ -505,35 +481,25 @@ impl DeviceManager { } if do_kb_backlight { - // TUF AURA LAPTOP DEVICE - // product_name = ASUS TUF Gaming F15 FX507ZE_FX507ZE - // product_family = ASUS TUF Gaming F15 - let product_name = DMIID::new().unwrap_or_default().product_name; - let product_family = DMIID::new().unwrap_or_default().product_family; - info!( - "No USB keyboard aura, system is {product_name}, try using sysfs backlight control" - ); - if product_name.contains("TUF") || product_family.contains("TUF") { - info!("TUF laptop, try using sysfs backlight control"); - if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(None, "tuf").await - && let DeviceHandle::Aura(aura) = dev_type.clone() + info!("No USB keyboard aura, try sysfs backlight / Dynamic Lighting"); + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(None, "tuf").await + && let DeviceHandle::Aura(aura) = dev_type.clone() + { + let path = dbus_path_for_tuf(); + let ctrl = AuraZbus::new(aura); + if ctrl + .start_tasks(connection, path.clone()) + .await + .map_err(|e| { + error!("Failed to start TUF Aura tasks: {e:?}, not adding this device") + }) + .is_ok() { - let path = dbus_path_for_tuf(); - let ctrl = AuraZbus::new(aura); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start TUF Aura tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: None, - }); - } + devices.push(AsusDevice { + device: dev_type, + dbus_path: path, + hid_key: None, + }); } } } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index c31de89a0..727507cb1 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -6,6 +6,7 @@ use rog_anime::AnimeType; use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; use rog_aura::AuraDeviceType; +use rog_platform::DynamicLed; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; @@ -41,17 +42,13 @@ pub enum DeviceHandle { /// The AniMe devices require USBRaw as they are not HID devices AniMe(AniMe), Scsi(ScsiAura), - Ally(Arc>), - OldAura(Arc>), - /// TUF laptops have an aditional set of attributes added to the LED /sysfs/ - TufLedClass(Arc>), /// TODO MulticolourLed, None, } impl DeviceHandle { - /// Try Slash HID. If one exists it is initialsed and returned. + /// Try Slash HID. If one exists it is initialised and returned. pub async fn new_slash_hid( device: Arc>, prod_id: &str, @@ -77,7 +74,7 @@ impl DeviceHandle { Ok(Self::Slash(slash)) } - /// Try Slash USB. If one exists it is initialsed and returned. + /// Try Slash USB. If one exists it is initialised and returned. pub async fn new_slash_usb() -> Result { debug!("Testing for USB Slash"); let slash_type = SlashType::from_dmi(); @@ -102,32 +99,6 @@ impl DeviceHandle { } } - /// Try AniMe Matrix HID. If one exists it is initialsed and returned. - pub async fn maybe_anime_hid( - _device: Arc>, - _prod_id: &str, - ) -> Result { - // TODO: can't use HIDRAW for anime at the moment - Err(RogError::NotFound( - "Can't use anime over hidraw yet. Skip.".to_string(), - )) - - // debug!("Testing for HIDRAW AniMe"); - // let anime_type = AnimeType::from_dmi(); - // dbg!(prod_id); - // if matches!(anime_type, AnimeType::Unsupported) || prod_id != "193b" - // { log::info!("Unknown or invalid AniMe: {prod_id:?}, - // skipping"); return Err(RogError::NotFound("No - // anime-matrix device".to_string())); } - // info!("Found AniMe Matrix HIDRAW {anime_type:?}: {prod_id}"); - - // let mut config = AniMeConfig::new().load(); - // config.anime_type = anime_type; - // let mut anime = AniMe::new(Some(device), None, - // Arc::new(Mutex::new(config))); anime.do_initialization(). - // await?; Ok(Self::AniMe(anime)) - } - pub async fn maybe_anime_usb() -> Result { debug!("Testing for USB AniMe"); let anime_type = get_anime_type(); @@ -142,7 +113,6 @@ impl DeviceHandle { let mut config = AniMeConfig::new().load(); config.anime_type = anime_type; let mut anime = AniMe::new( - None, Some(Arc::new(Mutex::new(usb))), Arc::new(Mutex::new(config)), ); @@ -197,11 +167,42 @@ impl DeviceHandle { Some(Arc::new(Mutex::new(k))) }); + // Check for Dynamic Lighting interface (keyboard / lightbar only). + let (dynamic_kbd, dynamic_lightbar) = { + let kbd = DynamicLed::find("asus::keyboard") + .map(|k| { + info!("Dynamic Lighting keyboard detected: asus::keyboard"); + Arc::new(Mutex::new(k)) + }) + .ok(); + let lb = DynamicLed::find("asus::lightbar") + .map(|l| { + info!("Dynamic Lighting lightbar detected: asus::lightbar"); + Arc::new(Mutex::new(l)) + }) + .ok(); + if kbd.is_some() || lb.is_some() { + (kbd, lb) + } else { + debug!("Dynamic Lighting not detected; using legacy hidraw fallback"); + (None, None) + } + }; + + // One owner: when DL validates, drop hidraw so two paths never fight. + let hid = if dynamic_kbd.is_some() || dynamic_lightbar.is_some() { + None + } else { + device + }; + // Load saved mode, colours, brightness, power from disk; apply on reload let mut config = AuraConfig::load_and_update_config(prod_id); config.led_type = aura_type; let aura = Aura { - hid: device, + dynamic_kbd, + dynamic_lightbar, + hid, backlight, config: Arc::new(Mutex::new(config)), }; diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4340c9385..279225281 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -24,6 +24,7 @@ # Usage - [asusctl](usage/asusctl.md) +- [Dynamic Lighting and legacy fallback](usage/dynamic-lighting.md) # FAQ diff --git a/docs/usage/dynamic-lighting.md b/docs/usage/dynamic-lighting.md new file mode 100644 index 000000000..92cf784b1 --- /dev/null +++ b/docs/usage/dynamic-lighting.md @@ -0,0 +1,60 @@ +# Dynamic Lighting and legacy fallback + +asusd prefers the Linux LED Dynamic Lighting ABI when a device exposes a valid +node below `/sys/class/leds`. A valid node must provide the generic `effect`, +`effect_index`, `zone_type`, and `led_count` attributes. Optional speed, +direction, palette, power-state, enable, and direct-buffer controls are used +only when the node advertises them. + +On supported ROG laptop keyboards, asusd retains the matching `hidraw` output +report as a capability-based fallback when no valid Dynamic Lighting node is +available. The two paths are mutually exclusive for a physical device: +Dynamic Lighting always wins, and hidraw is not used to retry an effect rejected +by the kernel. This preserves operation on released kernels while avoiding two +owners sending commands to the same controller. + +## Topology + +hid-asus registers independent chassis zones: + +| Node | Role | +|------|------| +| `asus::keyboard` | Keyboard (and logo power tracking) | +| `asus::lightbar` | Chassis lightbar when present | +| `asus::slash` | Slash LED when present | +| `asus::anime` | AniMe Matrix when present | + +There is no `asus::global` aggregate and no userspace `aura_mode` selector. +Each node accepts its own effect, palette, and direct RGB writes. Whole-device +requests fan out to every present zone. + +Lighting off is `enabled=false`, independent from `effect`. `brightness` stays +intensity (scaled from the D-Bus `LedBrightness` levels when +`max_brightness > 3`). Firmware effects, including static colour, are committed +with Aura `0xb3`/`0xb5`/`0xb4`; `enabled` gates the zone with `0xbd`. + +Kernel direct RGB may use HID LampArray (Usage Page `0x59`) when Aura `0xBC` +cannot drive the lightbar independently; firmware animations stay on Aura +`0xb3`. That backend choice is invisible to the sysfs ABI. + +## D-Bus zones + +The D-Bus API still describes historical keyboard and left/right lightbar +subzones. Under Dynamic Lighting those are not advertised +(`supported_basic_zones` is empty). Incoming legacy zone values are accepted +only as aliases: Key1–4 map to the keyboard node, BarLeft/BarRight to the +lightbar, and `None` fans out to every present zone. + +## Other devices + +ROG NVMe enclosure lighting requires the kernel ASUS Aura SCSI Dynamic Lighting +driver. asusd matches an enclosure's block-device ancestry to its exact LED +node and briefly retries while that node is being registered. It never falls +back to the first enclosure. The old public `rog_scsi` SG_IO API was removed +intentionally: vendor commands are kernel-owned, and applications must use the +Dynamic Lighting sysfs ABI. + +The optional generic `frame` attribute is not currently used by asusd. Direct +streaming uses `direct_buffer` and requires exactly `led_count * 3` RGB bytes. +Hardware-specific behavior still depends on the kernel driver reporting correct +topology, LED count, and optional attributes. diff --git a/rog-platform/src/hid_raw.rs b/rog-platform/src/hid_raw.rs index ec9c17373..bf5e679d6 100644 --- a/rog-platform/src/hid_raw.rs +++ b/rog-platform/src/hid_raw.rs @@ -21,6 +21,12 @@ pub struct HidRaw { } impl HidRaw { + /// Check whether a hidraw endpoint descriptor declares an output report ID. + pub fn supports_output_report(endpoint: &Device, report_id: u8) -> bool { + std::fs::read(endpoint.syspath().join("device/report_descriptor")) + .is_ok_and(|descriptor| descriptor_has_output_report(&descriptor, report_id)) + } + pub fn new(id_product: &str) -> Result { let mut enumerator = udev::Enumerator::new().map_err(|err| { warn!("{}", err); @@ -87,7 +93,7 @@ impl HidRaw { file: RefCell::new(OpenOptions::new().write(true).open(dev_node)?), devfs_path: dev_node.to_owned(), prod_id: id_product.to_string_lossy().into(), - _device_bcd: endpoint + _device_bcd: parent .attribute_value("bcdDevice") .unwrap_or_default() .to_string_lossy() @@ -106,12 +112,73 @@ impl HidRaw { /// Write an array of raw bytes to the device using the hidraw interface pub fn write_bytes(&self, message: &[u8]) -> Result<()> { - if let Ok(mut file) = self.file.try_borrow_mut() { - // TODO: re-get the file if error? - file.write_all(message).map_err(|e| { - PlatformError::IoPath(self.devfs_path.to_string_lossy().to_string(), e) - })?; + if message.is_empty() { + return Err(PlatformError::InvalidValue); + } + let mut file = self + .file + .try_borrow_mut() + .map_err(|_| PlatformError::InvalidValue)?; + file.write_all(message) + .map_err(|e| PlatformError::IoPath(self.devfs_path.to_string_lossy().to_string(), e)) + } +} + +fn descriptor_has_output_report(descriptor: &[u8], wanted_id: u8) -> bool { + let mut offset = 0; + let mut report_id = 0; + while let Some(prefix) = descriptor.get(offset).copied() { + if prefix == 0xfe { + let Some(size) = descriptor.get(offset + 1).copied() else { + return false; + }; + offset = match offset.checked_add(3 + usize::from(size)) { + Some(next) => next, + None => return false, + }; + continue; } - Ok(()) + let size = match prefix & 0x03 { + 3 => 4, + value => usize::from(value), + }; + let end = match offset.checked_add(1 + size) { + Some(end) => end, + None => return false, + }; + let Some(data) = descriptor.get(offset + 1..end) else { + return false; + }; + let item_type = (prefix >> 2) & 0x03; + let tag = prefix >> 4; + if item_type == 1 && tag == 8 && size == 1 { + report_id = data[0]; + } else if item_type == 0 && tag == 9 && report_id == wanted_id { + return true; + } + offset = end; + } + false +} + +#[cfg(test)] +mod tests { + use super::descriptor_has_output_report; + + #[test] + fn parses_output_report_id_safely() { + assert!(descriptor_has_output_report( + &[ + 0x85, 0x5d, 0x09, 0x01, 0x91, 0x02 + ], + 0x5d + )); + assert!(!descriptor_has_output_report( + &[ + 0x85, 0x5d, 0x09, 0x01, 0x81, 0x02 + ], + 0x5d + )); + assert!(!descriptor_has_output_report(&[0x85], 0x5d)); } } From 3fb016e66978af91db8287447e5513cdf6bd03cf Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 16 Oct 2026 11:21:53 +0200 Subject: [PATCH 5/9] feat(asusd): discover tuf dynamic lighting on asus::kbd_backlight Prefer aura:keyboard, then fall back to a Dynamic Lighting-capable asus::kbd_backlight from asus-wmi on TUF laptops. Keep brightness-only kbd_backlight as the legacy KeyboardBacklight path, and drop that handle when DL already owns the same LED node so the two paths never fight. Signed-off-by: Marco Scardovi --- asusd/src/aura_types.rs | 49 ++++++++++++++++++++++------------ docs/usage/dynamic-lighting.md | 9 +++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 727507cb1..823ee8214 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -160,35 +160,50 @@ impl DeviceHandle { } info!("Found laptop aura type {prod_id:?}"); - let backlight = KeyboardBacklight::new() - .map_err(|e| error!("Keyboard backlight error: {e:?}")) - .map_or(None, |k| { - info!("Found sysfs backlight control"); - Some(Arc::new(Mutex::new(k))) - }); - - // Check for Dynamic Lighting interface (keyboard / lightbar only). - let (dynamic_kbd, dynamic_lightbar) = { + // Prefer asus::keyboard; on TUF, asus-wmi may register the same DL ABI + // on asus::kbd_backlight when ASUS_WMI_DEVID_TUF_RGB_MODE* is present. + let (dynamic_kbd, dynamic_lightbar, dl_owns_kbd_backlight) = { let kbd = DynamicLed::find("asus::keyboard") - .map(|k| { - info!("Dynamic Lighting keyboard detected: asus::keyboard"); - Arc::new(Mutex::new(k)) - }) + .or_else(|_| DynamicLed::find("asus::kbd_backlight")) .ok(); + let dl_owns_kbd_backlight = kbd + .as_ref() + .is_some_and(|k| k.name().contains("kbd_backlight")); + let kbd = kbd.map(|k| { + info!("Dynamic Lighting keyboard detected: {}", k.name()); + Arc::new(Mutex::new(k)) + }); let lb = DynamicLed::find("asus::lightbar") .map(|l| { - info!("Dynamic Lighting lightbar detected: asus::lightbar"); + info!("Dynamic Lighting lightbar detected: {}", l.name()); Arc::new(Mutex::new(l)) }) .ok(); if kbd.is_some() || lb.is_some() { - (kbd, lb) + (kbd, lb, dl_owns_kbd_backlight) } else { - debug!("Dynamic Lighting not detected; using legacy hidraw fallback"); - (None, None) + debug!( + "Dynamic Lighting not detected; using legacy hidraw / TUF platform fallback" + ); + (None, None, false) } }; + // Brightness-only asus::kbd_backlight (or ally gamepad) stays available + // alongside asus::* DL nodes. If DL already owns that same LED name, + // do not keep a second KeyboardBacklight handle on it. + let backlight = if dl_owns_kbd_backlight { + debug!("Skipping legacy KeyboardBacklight; Dynamic Lighting owns kbd_backlight"); + None + } else { + KeyboardBacklight::new() + .map_err(|e| error!("Keyboard backlight error: {e:?}")) + .map_or(None, |k| { + info!("Found sysfs backlight control"); + Some(Arc::new(Mutex::new(k))) + }) + }; + // One owner: when DL validates, drop hidraw so two paths never fight. let hid = if dynamic_kbd.is_some() || dynamic_lightbar.is_some() { None diff --git a/docs/usage/dynamic-lighting.md b/docs/usage/dynamic-lighting.md index 92cf784b1..68e865b5b 100644 --- a/docs/usage/dynamic-lighting.md +++ b/docs/usage/dynamic-lighting.md @@ -23,6 +23,15 @@ hid-asus registers independent chassis zones: | `asus::lightbar` | Chassis lightbar when present | | `asus::slash` | Slash LED when present | | `asus::anime` | AniMe Matrix when present | +| `asus::kbd_backlight` | TUF WMI keyboard when asus-wmi exposes the DL ABI | + +asusd prefers `asus::keyboard`, then falls back to a Dynamic Lighting-capable +`asus::kbd_backlight`. Brightness-only `asus::kbd_backlight` (no +`effect`/`effect_index`/`zone_type`/`led_count`) is not Dynamic Lighting: it +remains the legacy TUF/`KeyboardBacklight` path for intensity and +`kbd_rgb_mode` / `kbd_rgb_state` when no DL keyboard node is present. When DL +owns `asus::kbd_backlight`, the legacy platform handle is not kept on that +node. There is no `asus::global` aggregate and no userspace `aura_mode` selector. Each node accepts its own effect, palette, and direct RGB writes. Whole-device From bdb4040618db0bacd470d9e97f53e4e2447039a0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:43:31 +0200 Subject: [PATCH 6/9] feat(rog-control-center): list every Aura mode and show supported ones Name the full Aura catalog in the mode menu, including Rainbow cycle, Rainbow wave, and Direct. Show only the mode numbers asusd reports for the current device. Signed-off-by: Marco Scardovi --- rog-control-center/ui/types/aura_types.slint | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rog-control-center/ui/types/aura_types.slint b/rog-control-center/ui/types/aura_types.slint index e6d4795fe..697c38764 100644 --- a/rog-control-center/ui/types/aura_types.slint +++ b/rog-control-center/ui/types/aura_types.slint @@ -46,9 +46,9 @@ export struct LaptopAuraPower { states: [AuraPowerState], } -// Modes with colour1: Static,Breathe,Star,Rain,Highlight,Laser,Ripple,Pulse,Comet,Flash (excl. Strobe,Rainbow,Nothing) -// Modes with colour2: Breathe, Star only. -// Speed: Breathe,Strobe,Rainbow,Star,Rain,Highlight,Laser,Ripple. Direction: Rainbow only. +// Colour 1: Static, Breathe, Star, Highlight, Laser, Ripple, Pulse, Comet, Flash. +// Colour 2: Breathe, Star. Speed: Breathe through Ripple. Direction: Rainbow wave. +// Index 9 is unused. The page shows only the modes the device reports. export global AuraPageData { in-out property <[string]> power_zone_names: [ @tr("Aura power zone" => "Logo"), @@ -77,8 +77,8 @@ export global AuraPageData { in-out property <[string]> mode_names: [ @tr("Basic aura mode" => "Static"), @tr("Basic aura mode" => "Breathe"), - @tr("Basic aura mode" => "Strobe"), - @tr("Basic aura mode" => "Rainbow"), + @tr("Basic aura mode" => "Rainbow cycle"), + @tr("Basic aura mode" => "Rainbow wave"), @tr("Basic aura mode" => "Star"), @tr("Basic aura mode" => "Rain"), @tr("Basic aura mode" => "Highlight"), @@ -88,8 +88,15 @@ export global AuraPageData { @tr("Basic aura mode" => "Pulse"), @tr("Basic aura mode" => "Comet"), @tr("Basic aura mode" => "Flash"), + @tr("Basic aura mode" => "Direct"), + ]; + in-out property <[string]> available_mode_names: [ + @tr("Basic aura mode" => "Static"), + @tr("Basic aura mode" => "Breathe"), + @tr("Basic aura mode" => "Rainbow cycle"), + @tr("Basic aura mode" => "Rainbow wave"), + @tr("Basic aura mode" => "Direct"), ]; - in-out property <[string]> available_mode_names: [ @tr("Basic aura mode" => "Static"), @tr("Basic aura mode" => "Breathe"), @tr("Basic aura mode" => "Strobe") ]; in-out property current_available_mode: 0; in-out property <[int]> supported_basic_modes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12]; in-out property <[string]> zone_names: [ From b8a0ef348424eb3f2603d958e38b5a627d732f34 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:43:31 +0200 Subject: [PATCH 7/9] chore: update translations --- .../translations/en/rog-control-center.po | 628 ++++++++++-------- 1 file changed, 335 insertions(+), 293 deletions(-) diff --git a/rog-control-center/translations/en/rog-control-center.po b/rog-control-center/translations/en/rog-control-center.po index f6f769a56..e75f4a827 100644 --- a/rog-control-center/translations/en/rog-control-center.po +++ b/rog-control-center/translations/en/rog-control-center.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-07-23 18:09+0000\n" +"POT-Creation-Date: 2026-10-26 07:36+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -12,6 +12,78 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +#: rog-control-center/ui/pages/about.slint:27 +msgctxt "PageAbout" +msgid "" +"\nA powerful graphical interface for managing ASUS ROG, TUF, and ProArt " +"laptops on Linux. It acts as the official GUI for the asusctl toolset, " +"allowing for seamless hardware tuning without the command line." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:32 +msgctxt "PageAbout" +msgid "Key Features:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:36 +msgctxt "PageAbout" +msgid "" +" • Performance: Switch power profiles and customize fan curves." +"\n • Aura Sync: Control keyboard backlighting and LED effects.\n " +"• Battery Health: Set charge limits to extend battery longevity.\n • " +"Display: Toggle Panel Overdrive and refresh rates.\n • AniMe Matrix: " +"Control AniMe Matrix displays." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:41 +msgctxt "PageAbout" +msgid "Requirements:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:46 +msgctxt "PageAbout" +msgid "" +" This software requires kernel version 6.19 or later. However due to " +"active development, the latest kernel is always the recommended one." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:51 +msgctxt "PageAbout" +msgid "Work in progress:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:55 +msgctxt "PageAbout" +msgid "" +" • Theme the widgets\n • Add a cpu/gpu temp/fan speed info " +"bar\n • Include fan speeds, temps in a bottom bar\n • Screenpad " +"controls\n • ROG Ally specific settings" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:60 +msgctxt "PageAbout" +msgid "License:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:64 +msgctxt "PageAbout" +msgid "" +" This project is open-source software licensed under the Mozilla Public " +"License 2.0 (MPL-2.0)." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:69 +msgctxt "PageAbout" +msgid "Links:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:73 +msgctxt "PageAbout" +msgid "" +" Source Code: https://github.com/OpenGamingCollective/asusctl\n " +"Website: https://asus-linux.org/" +msgstr "" + #: rog-control-center/ui/pages/anime.slint:6 msgctxt "Anime Brightness" msgid "Off" @@ -138,6 +210,71 @@ msgctxt "PageAnime" msgid "Off when on battery" msgstr "" +#: rog-control-center/ui/pages/app_settings.slint:45 +msgctxt "PageAppSettings" +msgid "Run in background after closing" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:53 +msgctxt "PageAppSettings" +msgid "Start app in background (UI closed)" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:61 +msgctxt "PageAppSettings" +msgid "Enable system tray icon" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:69 +msgctxt "PageAppSettings" +msgid "Enable dGPU notifications" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:77 +msgctxt "PageAppSettings" +msgid "Start app on system startup" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:87 +msgctxt "PageAppSettings" +msgid "Start in background on startup" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:97 +msgctxt "PageAppSettings" +msgid "Armory/ROG key to show/hide window (keep app running in background)" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:113 +msgctxt "PageAppSettings" +msgid "Setting up…" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:115 +msgctxt "PageAppSettings" +msgid "Shortcut active" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:117 +msgctxt "PageAppSettings" +msgid "No shortcut assigned" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:119 +msgctxt "PageAppSettings" +msgid "Global shortcuts unavailable" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:132 +msgctxt "PageAppSettings" +msgid "Set up shortcut" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:133 +msgctxt "PageAppSettings" +msgid "Configure shortcut" +msgstr "" + #: rog-control-center/ui/pages/aura.slint:29 msgctxt "PageAura" msgid "Brightness" @@ -183,6 +320,41 @@ msgctxt "PageAura" msgid "Power Zones" msgstr "" +#: rog-control-center/ui/pages/battery.slint:24 +msgctxt "PageBattery" +msgid "Battery Information" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:29 +msgctxt "PageBattery" +msgid "Charge limit" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:54 +msgctxt "PageBattery" +msgid "Battery Health (Capacity):" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:67 +msgctxt "PageBattery" +msgid "Status:" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:80 +msgctxt "PageBattery" +msgid "Power Draw:" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:93 +msgctxt "PageBattery" +msgid "Time to Full:" +msgstr "" + +#: rog-control-center/ui/pages/battery.slint:93 +msgctxt "PageBattery" +msgid "Time Remaining:" +msgstr "" + #: rog-control-center/ui/pages/fans.slint:27 msgctxt "FanTab" msgid "This fan is not avilable on this machine" @@ -244,6 +416,67 @@ msgctxt "PageFans" msgid "Quiet" msgstr "" +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Integrated" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Hybrid" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Ultimate" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:48 +msgctxt "PageGPU" +msgid "Internal/Discrete GPU" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:57 +msgctxt "PageGPU" +msgid "This device has no switchable GPU." +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:69 +msgctxt "PageGPU" +msgid "" +"Note: Changes take effect after a reboot. The dropdown always shows the " +"current mode." +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:76 +msgctxt "PageGPU" +msgid "GPU mode" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:97 +msgctxt "PageGPU" +msgid "Note: Reserved GPU memory changes take effect after a reboot." +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:104 +msgctxt "PageGPU" +msgid "Reserved GPU memory" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:118 +msgctxt "PageGPU" +msgid "XG Mobile LED" +msgstr "" + +#: rog-control-center/ui/pages/gpu.slint:131 +msgctxt "PageGPU" +msgid "Disable nvidia-powerd on battery" +msgstr "" + #: rog-control-center/ui/pages/slash.slint:51 msgctxt "PageSlash" msgid "Slash lighting" @@ -311,143 +544,6 @@ msgid "" "models." msgstr "" -#: rog-control-center/ui/pages/battery.slint:24 -msgctxt "PageBattery" -msgid "Battery Information" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:29 -msgctxt "PageBattery" -msgid "Charge limit" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:54 -msgctxt "PageBattery" -msgid "Battery Health (Capacity):" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:67 -msgctxt "PageBattery" -msgid "Status:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:80 -msgctxt "PageBattery" -msgid "Power Draw:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:93 -msgctxt "PageBattery" -msgid "Time to Full:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:93 -msgctxt "PageBattery" -msgid "Time Remaining:" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:27 -msgctxt "PageAppSettings" -msgid "Run in background after closing" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:35 -msgctxt "PageAppSettings" -msgid "Start app in background (UI closed)" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:43 -msgctxt "PageAppSettings" -msgid "Enable system tray icon" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:51 -msgctxt "PageAppSettings" -msgid "Enable dGPU notifications" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:59 -msgctxt "PageAppSettings" -msgid "Start app on system startup" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:69 -msgctxt "PageAppSettings" -msgid "Start in background on startup" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:27 -msgctxt "PageAbout" -msgid "" -"\nA powerful graphical interface for managing ASUS ROG, TUF, and ProArt " -"laptops on Linux. It acts as the official GUI for the asusctl toolset, " -"allowing for seamless hardware tuning without the command line." -msgstr "" - -#: rog-control-center/ui/pages/about.slint:32 -msgctxt "PageAbout" -msgid "Key Features:" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:36 -msgctxt "PageAbout" -msgid "" -" • Performance: Switch power profiles and customize fan curves." -"\n • Aura Sync: Control keyboard backlighting and LED effects.\n " -"• Battery Health: Set charge limits to extend battery longevity.\n • " -"Display: Toggle Panel Overdrive and refresh rates.\n • AniMe Matrix: " -"Control AniMe Matrix displays." -msgstr "" - -#: rog-control-center/ui/pages/about.slint:41 -msgctxt "PageAbout" -msgid "Requirements:" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:46 -msgctxt "PageAbout" -msgid "" -" This software requires kernel version 6.19 or later. However due to " -"active development, the latest kernel is always the recommended one." -msgstr "" - -#: rog-control-center/ui/pages/about.slint:51 -msgctxt "PageAbout" -msgid "Work in progress:" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:55 -msgctxt "PageAbout" -msgid "" -" • Theme the widgets\n • Add a cpu/gpu temp/fan speed info " -"bar\n • Include fan speeds, temps in a bottom bar\n • Screenpad " -"controls\n • ROG Ally specific settings" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:60 -msgctxt "PageAbout" -msgid "License:" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:64 -msgctxt "PageAbout" -msgid "" -" This project is open-source software licensed under the Mozilla Public " -"License 2.0 (MPL-2.0)." -msgstr "" - -#: rog-control-center/ui/pages/about.slint:69 -msgctxt "PageAbout" -msgid "Links:" -msgstr "" - -#: rog-control-center/ui/pages/about.slint:73 -msgctxt "PageAbout" -msgid "" -" Source Code: https://github.com/OpenGamingCollective/asusctl\n " -"Website: https://asus-linux.org/" -msgstr "" - #: rog-control-center/ui/pages/system.slint:21 msgctxt "SystemPageData" msgid "Balanced" @@ -512,401 +608,340 @@ msgctxt "SystemPageData" msgid "Multi-zone Strong" msgstr "" -#: rog-control-center/ui/pages/system.slint:213 +#: rog-control-center/ui/pages/system.slint:214 msgctxt "PageSystem" msgid "Power settings" msgstr "" -#: rog-control-center/ui/pages/system.slint:235 +#: rog-control-center/ui/pages/system.slint:236 msgctxt "PageSystem" msgid "Hardware Monitor" msgstr "" -#: rog-control-center/ui/pages/system.slint:248 +#: rog-control-center/ui/pages/system.slint:249 msgctxt "PageSystem" msgid "CPU Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:257 -#: rog-control-center/ui/pages/system.slint:257 -#: rog-control-center/ui/pages/system.slint:257 +#: rog-control-center/ui/pages/system.slint:258 +#: rog-control-center/ui/pages/system.slint:258 +#: rog-control-center/ui/pages/system.slint:258 msgctxt "PageSystem" msgid "Temperature:" msgstr "" -#: rog-control-center/ui/pages/system.slint:270 -#: rog-control-center/ui/pages/system.slint:270 -#: rog-control-center/ui/pages/system.slint:270 +#: rog-control-center/ui/pages/system.slint:271 +#: rog-control-center/ui/pages/system.slint:271 +#: rog-control-center/ui/pages/system.slint:271 msgctxt "PageSystem" msgid "Usage:" msgstr "" -#: rog-control-center/ui/pages/system.slint:283 +#: rog-control-center/ui/pages/system.slint:284 msgctxt "PageSystem" msgid "Frequency:" msgstr "" -#: rog-control-center/ui/pages/system.slint:296 +#: rog-control-center/ui/pages/system.slint:297 msgctxt "PageSystem" msgid "iGPU Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 msgctxt "PageSystem" msgid "N/A" msgstr "" -#: rog-control-center/ui/pages/system.slint:331 +#: rog-control-center/ui/pages/system.slint:332 msgctxt "PageSystem" msgid "Memory Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:340 +#: rog-control-center/ui/pages/system.slint:341 msgctxt "PageSystem" msgid "RAM Usage:" msgstr "" -#: rog-control-center/ui/pages/system.slint:359 +#: rog-control-center/ui/pages/system.slint:360 msgctxt "PageSystem" msgid "dGPU Status" msgstr "" #: rog-control-center/ui/pages/system.slint:373 -#: rog-control-center/ui/pages/system.slint:386 +#: rog-control-center/ui/pages/system.slint:373 msgctxt "PageSystem" msgid "Suspended" msgstr "" -#: rog-control-center/ui/pages/system.slint:394 +#: rog-control-center/ui/pages/system.slint:395 msgctxt "PageSystem" msgid "Fan Speeds" msgstr "" -#: rog-control-center/ui/pages/system.slint:403 +#: rog-control-center/ui/pages/system.slint:404 msgctxt "PageSystem" msgid "CPU Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:416 +#: rog-control-center/ui/pages/system.slint:417 msgctxt "PageSystem" msgid "GPU Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:429 +#: rog-control-center/ui/pages/system.slint:430 msgctxt "PageSystem" msgid "Mid Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:448 +#: rog-control-center/ui/pages/system.slint:449 msgctxt "PageSystem" msgid "Platform Profile" msgstr "" -#: rog-control-center/ui/pages/system.slint:458 +#: rog-control-center/ui/pages/system.slint:459 msgctxt "PageSystem" msgid "Advanced" msgstr "" -#: rog-control-center/ui/pages/system.slint:478 +#: rog-control-center/ui/pages/system.slint:479 msgctxt "PageSystem" msgid "Screenpad brightness" msgstr "" -#: rog-control-center/ui/pages/system.slint:502 +#: rog-control-center/ui/pages/system.slint:503 msgctxt "PageSystem" msgid "Sync with primary" msgstr "" -#: rog-control-center/ui/pages/system.slint:522 +#: rog-control-center/ui/pages/system.slint:523 msgctxt "PageSystem" msgid "Armoury settings" msgstr "" -#: rog-control-center/ui/pages/system.slint:532 +#: rog-control-center/ui/pages/system.slint:533 msgctxt "no_asus_armoury_driver_1" msgid "The asus-armoury driver is not loaded" msgstr "" -#: rog-control-center/ui/pages/system.slint:539 +#: rog-control-center/ui/pages/system.slint:540 msgctxt "no_asus_armoury_driver_2" msgid "For advanced features you will require a kernel with this driver added." msgstr "" -#: rog-control-center/ui/pages/system.slint:551 +#: rog-control-center/ui/pages/system.slint:552 msgctxt "PageSystem" msgid "Panel Overdrive" msgstr "" -#: rog-control-center/ui/pages/system.slint:559 +#: rog-control-center/ui/pages/system.slint:560 msgctxt "PageSystem" msgid "POST boot sound" msgstr "" -#: rog-control-center/ui/pages/system.slint:567 +#: rog-control-center/ui/pages/system.slint:568 msgctxt "PageSystem" msgid "Screen Auto Brightness" msgstr "" -#: rog-control-center/ui/pages/system.slint:575 +#: rog-control-center/ui/pages/system.slint:576 msgctxt "PageSystem" msgid "MCU Powersave" msgstr "" -#: rog-control-center/ui/pages/system.slint:584 +#: rog-control-center/ui/pages/system.slint:585 msgctxt "PageSystem" msgid "MiniLED Mode" msgstr "" -#: rog-control-center/ui/pages/system.slint:602 +#: rog-control-center/ui/pages/system.slint:603 msgctxt "ppt_warning" msgid "The following settings are not applied until the toggle is enabled." msgstr "" -#: rog-control-center/ui/pages/system.slint:607 -#: rog-control-center/ui/pages/system.slint:607 +#: rog-control-center/ui/pages/system.slint:608 +#: rog-control-center/ui/pages/system.slint:608 msgctxt "ppt_group_enabled" msgid "Enable Tuning" msgstr "" -#: rog-control-center/ui/pages/system.slint:630 +#: rog-control-center/ui/pages/system.slint:631 msgctxt "requires_fan_curve_warning" msgid "" "CPU power limits require an active custom fan curve. Enable a fan curve for " "the active profile in the Fans tab to unlock these controls." msgstr "" -#: rog-control-center/ui/pages/system.slint:640 -#: rog-control-center/ui/pages/system.slint:640 +#: rog-control-center/ui/pages/system.slint:641 +#: rog-control-center/ui/pages/system.slint:641 msgctxt "ppt_pl1_spl" msgid "CPU Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:642 +#: rog-control-center/ui/pages/system.slint:643 msgctxt "ppt_pl1_spl_help" msgid "" "Long-term CPU power limit that affects sustained workload performance. " "Higher values may increase heat and power consumption." msgstr "" -#: rog-control-center/ui/pages/system.slint:658 -#: rog-control-center/ui/pages/system.slint:658 +#: rog-control-center/ui/pages/system.slint:659 +#: rog-control-center/ui/pages/system.slint:659 msgctxt "ppt_pl2_sppt" msgid "CPU Turbo Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:660 +#: rog-control-center/ui/pages/system.slint:661 msgctxt "ppt_pl2_sppt_help" msgid "" "Short-term CPU power limit for boost periods. Controls maximum power during " "brief high-performance bursts." msgstr "" -#: rog-control-center/ui/pages/system.slint:676 -#: rog-control-center/ui/pages/system.slint:676 +#: rog-control-center/ui/pages/system.slint:677 +#: rog-control-center/ui/pages/system.slint:677 msgctxt "ppt_pl3_fppt" msgid "CPU Fast Burst Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:678 +#: rog-control-center/ui/pages/system.slint:679 msgctxt "ppt_pl3_fppt_help" msgid "" "Ultra-short duration power limit for instantaneous CPU bursts. Affects " "responsiveness during sudden workload spikes." msgstr "" -#: rog-control-center/ui/pages/system.slint:693 -#: rog-control-center/ui/pages/system.slint:693 +#: rog-control-center/ui/pages/system.slint:694 +#: rog-control-center/ui/pages/system.slint:694 msgctxt "ppt_fppt" msgid "Fast Package Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:695 +#: rog-control-center/ui/pages/system.slint:696 msgctxt "ppt_fppt_help" msgid "" "Ultra-short duration power limit for system package. Controls maximum power " "during millisecond-scale load spikes." msgstr "" -#: rog-control-center/ui/pages/system.slint:711 -#: rog-control-center/ui/pages/system.slint:711 +#: rog-control-center/ui/pages/system.slint:712 +#: rog-control-center/ui/pages/system.slint:712 msgctxt "ppt_apu_sppt" msgid "APU Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:713 +#: rog-control-center/ui/pages/system.slint:714 msgctxt "ppt_apu_sppt_help" msgid "" "Long-term power limit for integrated graphics and CPU combined. Affects " "sustained performance of APU-based workloads." msgstr "" -#: rog-control-center/ui/pages/system.slint:729 -#: rog-control-center/ui/pages/system.slint:729 +#: rog-control-center/ui/pages/system.slint:730 +#: rog-control-center/ui/pages/system.slint:730 msgctxt "ppt_platform_sppt" msgid "Platform Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:731 +#: rog-control-center/ui/pages/system.slint:732 msgctxt "ppt_platform_sppt_help" msgid "" "Overall system power limit for sustained operations. Controls total platform " "power consumption over extended periods." msgstr "" -#: rog-control-center/ui/pages/system.slint:747 -#: rog-control-center/ui/pages/system.slint:747 +#: rog-control-center/ui/pages/system.slint:748 +#: rog-control-center/ui/pages/system.slint:748 msgctxt "nv_dynamic_boost" msgid "GPU Power Boost" msgstr "" -#: rog-control-center/ui/pages/system.slint:749 +#: rog-control-center/ui/pages/system.slint:750 msgctxt "nv_dynamic_boost_help" msgid "" "Additional power allocation for GPU dynamic boost. Higher values increase " "GPU performance but generate more heat." msgstr "" -#: rog-control-center/ui/pages/system.slint:765 -#: rog-control-center/ui/pages/system.slint:765 +#: rog-control-center/ui/pages/system.slint:766 +#: rog-control-center/ui/pages/system.slint:766 msgctxt "nv_tgp" msgid "GPU TGP" msgstr "" -#: rog-control-center/ui/pages/system.slint:767 +#: rog-control-center/ui/pages/system.slint:768 msgctxt "nv_tgp_help" msgid "" "Total graphics power limit for the dedicated NVIDIA GPU. Higher values can " "improve performance at the cost of additional heat and power draw." msgstr "" -#: rog-control-center/ui/pages/system.slint:783 -#: rog-control-center/ui/pages/system.slint:783 +#: rog-control-center/ui/pages/system.slint:784 +#: rog-control-center/ui/pages/system.slint:784 msgctxt "nv_temp_target" msgid "GPU Temperature Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:785 +#: rog-control-center/ui/pages/system.slint:786 msgctxt "nv_temp_target_help" msgid "" "Maximum GPU temperature threshold in Celsius. GPU will throttle to maintain " "temperature below this limit." msgstr "" -#: rog-control-center/ui/pages/system.slint:839 +#: rog-control-center/ui/pages/system.slint:840 msgctxt "PageSystem" msgid "Energy Performance Preference linked to Throttle Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:843 +#: rog-control-center/ui/pages/system.slint:844 msgctxt "PageSystem" msgid "Change EPP based on Throttle Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:851 +#: rog-control-center/ui/pages/system.slint:852 msgctxt "PageSystem" msgid "EPP for Balanced Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:861 +#: rog-control-center/ui/pages/system.slint:862 msgctxt "PageSystem" msgid "EPP for Performance Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:871 +#: rog-control-center/ui/pages/system.slint:872 msgctxt "PageSystem" msgid "EPP for " msgstr "" -#: rog-control-center/ui/pages/system.slint:871 +#: rog-control-center/ui/pages/system.slint:872 msgctxt "PageSystem" msgid " Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:890 +#: rog-control-center/ui/pages/system.slint:891 msgctxt "PageSystem" msgid "Throttle Policy for power state" msgstr "" -#: rog-control-center/ui/pages/system.slint:896 +#: rog-control-center/ui/pages/system.slint:897 msgctxt "PageSystem" msgid "Throttle Policy on Battery" msgstr "" -#: rog-control-center/ui/pages/system.slint:906 -#: rog-control-center/ui/pages/system.slint:906 +#: rog-control-center/ui/pages/system.slint:907 +#: rog-control-center/ui/pages/system.slint:907 msgctxt "PageSystem" msgid "Enabled" msgstr "" -#: rog-control-center/ui/pages/system.slint:917 +#: rog-control-center/ui/pages/system.slint:918 msgctxt "PageSystem" msgid "Throttle Policy on AC" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Integrated" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Hybrid" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Ultimate" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:48 -msgctxt "PageGPU" -msgid "Internal/Discrete GPU" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:57 -msgctxt "PageGPU" -msgid "This device has no switchable GPU." -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:69 -msgctxt "PageGPU" -msgid "" -"Note: Changes take effect after a reboot. The dropdown always shows the " -"current mode." -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:76 -msgctxt "PageGPU" -msgid "GPU mode" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:97 -msgctxt "PageGPU" -msgid "Note: Reserved GPU memory changes take effect after a reboot." -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:104 -msgctxt "PageGPU" -msgid "Reserved GPU memory" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:118 -msgctxt "PageGPU" -msgid "XG Mobile LED" -msgstr "" - -#: rog-control-center/ui/pages/gpu.slint:131 -msgctxt "PageGPU" -msgid "Disable nvidia-powerd on battery" -msgstr "" - #: rog-control-center/ui/types/aura_types.slint:54 msgctxt "Aura power zone" msgid "Logo" @@ -980,12 +1015,13 @@ msgstr "" #: rog-control-center/ui/types/aura_types.slint:80 #: rog-control-center/ui/types/aura_types.slint:80 msgctxt "Basic aura mode" -msgid "Strobe" +msgid "Rainbow cycle" msgstr "" +#: rog-control-center/ui/types/aura_types.slint:81 #: rog-control-center/ui/types/aura_types.slint:81 msgctxt "Basic aura mode" -msgid "Rainbow" +msgid "Rainbow wave" msgstr "" #: rog-control-center/ui/types/aura_types.slint:82 @@ -1033,77 +1069,83 @@ msgctxt "Basic aura mode" msgid "Flash" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:96 +#: rog-control-center/ui/types/aura_types.slint:91 +#: rog-control-center/ui/types/aura_types.slint:91 +msgctxt "Basic aura mode" +msgid "Direct" +msgstr "" + +#: rog-control-center/ui/types/aura_types.slint:103 msgctxt "Aura zone" msgid "None" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:97 +#: rog-control-center/ui/types/aura_types.slint:104 msgctxt "Aura zone" msgid "Key1" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:98 +#: rog-control-center/ui/types/aura_types.slint:105 msgctxt "Aura zone" msgid "Key2" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:99 +#: rog-control-center/ui/types/aura_types.slint:106 msgctxt "Aura zone" msgid "Key3" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:100 +#: rog-control-center/ui/types/aura_types.slint:107 msgctxt "Aura zone" msgid "Key4" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:101 +#: rog-control-center/ui/types/aura_types.slint:108 msgctxt "Aura zone" msgid "Logo" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:102 +#: rog-control-center/ui/types/aura_types.slint:109 msgctxt "Aura zone" msgid "Lightbar Left" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:103 +#: rog-control-center/ui/types/aura_types.slint:110 msgctxt "Aura zone" msgid "Lightbar Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:106 +#: rog-control-center/ui/types/aura_types.slint:113 msgctxt "Aura direction" msgid "Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:107 +#: rog-control-center/ui/types/aura_types.slint:114 msgctxt "Aura direction" msgid "Left" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:108 +#: rog-control-center/ui/types/aura_types.slint:115 msgctxt "Aura direction" msgid "Up" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:109 +#: rog-control-center/ui/types/aura_types.slint:116 msgctxt "Aura direction" msgid "Down" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:112 +#: rog-control-center/ui/types/aura_types.slint:119 msgctxt "Aura speed" msgid "Low" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:113 +#: rog-control-center/ui/types/aura_types.slint:120 msgctxt "Aura speed" msgid "Medium" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:114 +#: rog-control-center/ui/types/aura_types.slint:121 msgctxt "Aura speed" msgid "High" msgstr "" From c0a982aa90da7e210a5451530d65c513e83ddb46 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Sat, 12 Sep 2026 19:13:14 +0200 Subject: [PATCH 8/9] feat(asusd): control slash lighting via sysfs led classdev Drive Slash lighting through the kernel LED classdev sysfs path when asus::slash exists. Do not gate on DMI board lists. Kernel Slash LED has no power_states, so boot/sleep/shutdown/battery/lid D-Bus setters return NotSupported instead of persisting a hardware no-op. Signed-off-by: Marco Scardovi --- Cargo.lock | 1 - asusd/src/aura_manager.rs | 119 ++++++----------- asusd/src/aura_slash/mod.rs | 51 ++------ asusd/src/aura_slash/trait_impls.rs | 191 ++++++++-------------------- asusd/src/aura_types.rs | 63 ++------- rog-platform/src/lib.rs | 2 + rog-platform/src/slash_led.rs | 65 ++++++++++ rog-slash/Cargo.toml | 4 +- rog-slash/src/data.rs | 35 +---- 9 files changed, 188 insertions(+), 343 deletions(-) create mode 100644 rog-platform/src/slash_led.rs diff --git a/Cargo.lock b/Cargo.lock index 412dec6db..181151c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4812,7 +4812,6 @@ dependencies = [ name = "rog_slash" version = "6.5.0" dependencies = [ - "dmi_id", "serde", "thiserror 2.0.21", "zbus", diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index 7cebf5caf..00187c11d 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -4,11 +4,12 @@ // - Add it to Zbus server // - If udev sees device removed then remove the zbus path -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::sync::Arc; use log::{debug, error, info, warn}; use mio::{Events, Interest, Poll, Token}; +use rog_aura::AuraDeviceType; use rog_platform::error::PlatformError; use rog_platform::hid_raw::HidRaw; use tokio::sync::Mutex; @@ -96,7 +97,6 @@ pub struct AsusDevice { pub struct DeviceManager { _dbus_connection: Connection, - _hid_handles: Arc>>>>, } /// Returns true if this hidraw device is a non-Aura interface on the @@ -131,30 +131,9 @@ fn is_non_aura_1ce6_interface(device: &Device) -> bool { } impl DeviceManager { - #[allow(clippy::type_complexity)] - async fn get_or_create_hid_handle( - handles: &Arc>>>>, - endpoint: &Device, - ) -> Result<(Arc>, String), RogError> { - let dev_node = endpoint - .devnode() - .ok_or_else(|| RogError::MissingFunction("hidraw devnode missing".to_string()))?; - let key = dev_node.to_string_lossy().to_string(); - - if let Some(existing) = handles.lock().await.get(&key).cloned() { - return Ok((existing, key)); - } - - let hidraw = HidRaw::from_device(endpoint.clone())?; - let handle = Arc::new(Mutex::new(hidraw)); - handles.lock().await.insert(key.clone(), handle.clone()); - Ok((handle, key)) - } - async fn init_hid_devices( connection: &Connection, device: Device, - handles: Arc>>>>, ) -> Result, RogError> { let mut devices = Vec::new(); if let Some(usb_device) = device.parent_with_subsystem_devtype("usb", "usb_device")? @@ -169,40 +148,31 @@ impl DeviceManager { // So let's see what we have and: // 1. Generate an interface path // 2. Create the device - // Use the top-level endpoint, not the parent - if let Ok((dev, hid_key)) = Self::get_or_create_hid_handle(&handles, &device).await { - debug!("Testing device {usb_id:?}"); - // SLASH DEVICE - if let Ok(dev_type) = - DeviceHandle::new_slash_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Slash(slash) = dev_type.clone() - { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); - let ctrl = SlashZbus::new(slash); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start Slash tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } - } - // AURA LAPTOP DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Aura(aura) = dev_type.clone() + let usb_id_str = usb_id.to_str().unwrap_or_default(); + let aura_type = AuraDeviceType::from(usb_id_str); + if matches!( + aura_type, + AuraDeviceType::LaptopKeyboard2021 + | AuraDeviceType::LaptopKeyboardPre2021 + | AuraDeviceType::LaptopKeyboardTuf + | AuraDeviceType::Ally + ) { + let hid_node = device + .devnode() + .map(|path| path.to_string_lossy().into_owned()); + let hid = HidRaw::from_device(device) + .map(|hid| Arc::new(Mutex::new(hid))) + .ok(); + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(hid, usb_id_str).await + && let DeviceHandle::Aura(aura) = &dev_type { + // Dynamic Lighting owns the effect path and drops hidraw. + // Do not key hotplug teardown on the probe hidraw node, or a + // remove uevent (s2idle / re-enumeration) would unregister + // the D-Bus Aura object while sysfs DL nodes remain valid. + let hid_key = if aura.hid.is_some() { hid_node } else { None }; let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); - let ctrl = AuraZbus::new(aura); + let ctrl = AuraZbus::new(aura.clone()); if ctrl .start_tasks(connection, path.clone()) .await @@ -214,22 +184,18 @@ impl DeviceManager { devices.push(AsusDevice { device: dev_type, dbus_path: path, - hid_key: Some(hid_key), + hid_key, }); } } - } else { - warn!("Failed to initialise shared hid handle for {usb_id:?}"); + return Ok(devices); } } Ok(devices) } /// To be called on daemon startup - async fn init_all_hid( - connection: &Connection, - handles: Arc>>>>, - ) -> Result, RogError> { + async fn init_all_hid(connection: &Connection) -> Result, RogError> { // Ensure we only process one hidraw interface per physical USB device. // A USB device can expose multiple HID interfaces (and thus multiple hidraw nodes). // Processing more than one causes duplicate device initialisation which can @@ -266,7 +232,7 @@ impl DeviceManager { } } - devices.append(&mut Self::init_hid_devices(connection, device, handles.clone()).await?); + devices.append(&mut Self::init_hid_devices(connection, device).await?); } Ok(devices) @@ -406,13 +372,10 @@ impl DeviceManager { Ok(devices) } - pub async fn find_all_devices( - connection: &Connection, - handles: Arc>>>>, - ) -> Vec { + pub async fn find_all_devices(connection: &Connection) -> Vec { let mut devices: Vec = Vec::new(); // HID first, always - if let Ok(devs) = &mut Self::init_all_hid(connection, handles.clone()).await { + if let Ok(devs) = &mut Self::init_all_hid(connection).await { devices.append(devs); } // USB after, need to check if HID picked something up and if so, skip it @@ -432,7 +395,7 @@ impl DeviceManager { } if do_slash { - if let Ok(dev_type) = DeviceHandle::new_slash_usb().await { + if let Ok(dev_type) = DeviceHandle::maybe_slash().await { if let DeviceHandle::Slash(slash) = dev_type.clone() { let path = dbus_path_for_slash(); let ctrl = SlashZbus::new(slash); @@ -513,19 +476,16 @@ impl DeviceManager { pub async fn new(connection: Connection) -> Result { let conn_copy = connection.clone(); - let hid_handles = Arc::new(Mutex::new(HashMap::new())); - let devices = Self::find_all_devices(&conn_copy, hid_handles.clone()).await; + let devices = Self::find_all_devices(&conn_copy).await; info!("Found {} valid devices on startup", devices.len()); let devices = Arc::new(Mutex::new(devices)); let manager = Self { _dbus_connection: connection, - _hid_handles: hid_handles.clone(), }; // TODO: The /sysfs/ LEDs don't cause events, so they need to be manually // checked for and added - let hid_handles_thread = hid_handles.clone(); std::thread::spawn(move || { let mut monitor = MonitorBuilder::new()?.listen()?; let mut poll = Poll::new()?; @@ -554,7 +514,6 @@ impl DeviceManager { let devices = devices.clone(); let conn_copy = conn_copy.clone(); - let hid_handles = hid_handles_thread.clone(); rt.block_on(async move { // SCSCI devs if subsys == "block" { @@ -672,11 +631,6 @@ impl DeviceManager { }; info!("AuraManager removed: {path:?}, {res}"); } - // Always drop the shared handle for this node, even if no - // AsusDevice referenced it, so the fd (and minor) is freed. - if hid_handles.lock().await.remove(&removed_node).is_some() { - info!("Dropped hid handle for {removed_node}"); - } } } else if action == "add" && let Some(parent) = @@ -699,10 +653,9 @@ impl DeviceManager { if is_non_aura_1ce6_interface(&evdev) { return Ok(()); } - if let Ok(mut new_devs) = - Self::init_hid_devices(&conn_copy, evdev, hid_handles.clone()) - .await - .map_err(|e| error!("Couldn't add new device: {e:?}")) + if let Ok(mut new_devs) = Self::init_hid_devices(&conn_copy, evdev) + .await + .map_err(|e| error!("Couldn't add new device: {e:?}")) { devices.lock().await.append(&mut new_devs); } diff --git a/asusd/src/aura_slash/mod.rs b/asusd/src/aura_slash/mod.rs index 8996f59e7..b725eb73e 100644 --- a/asusd/src/aura_slash/mod.rs +++ b/asusd/src/aura_slash/mod.rs @@ -1,9 +1,7 @@ use std::sync::Arc; use config::SlashConfig; -use rog_platform::hid_raw::HidRaw; -use rog_platform::usb_raw::USBRaw; -use rog_slash::usb::{slash_pkt_enable, slash_pkt_init, slash_pkt_options, slash_pkt_set_mode}; +use rog_platform::slash_led::SlashLed; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -13,57 +11,32 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Slash { - hid: Option>>, - usb: Option>>, + led: SlashLed, config: Arc>, } impl Slash { - pub fn new( - hid: Option>>, - usb: Option>>, - config: Arc>, - ) -> Self { - Self { hid, usb, config } + pub fn new(led: SlashLed, config: Arc>) -> Self { + Self { led, config } } - pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { - self.config.lock().await + pub fn led(&self) -> &SlashLed { + &self.led } - pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; - } - Ok(()) + pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { + self.config.lock().await } /// Initialise the device if required. Locks the internal config so be wary /// of deadlocks. pub async fn do_initialization(&self) -> Result<(), RogError> { - // Don't try to initialise these models as the asus drivers already did let config = self.config.lock().await; - for pkt in &slash_pkt_init(config.slash_type) { - self.write_bytes(pkt).await?; - } - self.write_bytes(&slash_pkt_enable(config.slash_type, config.enabled)) - .await?; - - // Apply config upon initialization - let option_packets = slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - ); - self.write_bytes(&option_packets).await?; - - let mode_packets = slash_pkt_set_mode(config.slash_type, config.display_mode); - // self.node.write_bytes(&mode_packets[0])?; - self.write_bytes(&mode_packets[1]).await?; + let brightness = if config.enabled { config.brightness } else { 0 }; + self.led.set_brightness(brightness)?; + self.led.set_slash_interval(config.display_interval)?; + self.led.set_slash_mode(&config.display_mode.to_string())?; Ok(()) } } diff --git a/asusd/src/aura_slash/trait_impls.rs b/asusd/src/aura_slash/trait_impls.rs index 70aa1ad36..0ad65ab26 100644 --- a/asusd/src/aura_slash/trait_impls.rs +++ b/asusd/src/aura_slash/trait_impls.rs @@ -1,10 +1,5 @@ use config_traits::StdConfig; use log::{debug, error, warn}; -use rog_slash::usb::{ - slash_pkt_battery_saver, slash_pkt_boot, slash_pkt_enable, slash_pkt_lid_closed, - slash_pkt_low_battery, slash_pkt_options, slash_pkt_save, slash_pkt_set_mode, - slash_pkt_shutdown, slash_pkt_sleep, -}; use rog_slash::{DeviceState, SlashMode}; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; @@ -13,6 +8,15 @@ use super::Slash; use crate::Reloadable; use crate::error::RogError; +const SLASH_POWER_UNSUPPORTED: &str = concat!( + "Kernel Slash LED has no power_states; ", + "boot/sleep/shutdown/battery/lid persistence is not supported" +); + +fn slash_power_not_supported() -> zbus::fdo::Error { + zbus::fdo::Error::NotSupported(SLASH_POWER_UNSUPPORTED.into()) +} + #[derive(Clone)] pub struct SlashZbus(Slash); @@ -26,7 +30,6 @@ impl SlashZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -34,9 +37,11 @@ impl SlashZbus { .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } @@ -58,25 +63,11 @@ impl SlashZbus { } else { config.brightness }; - self.0 - .write_bytes(&slash_pkt_enable(config.slash_type, enabled)) - .await - .map_err(|err| { - warn!("ctrl_slash::enable {}", err); - }) - .ok(); - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - enabled, - brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + let b = if enabled { brightness } else { 0 }; + if let Err(err) = self.0.led().set_brightness(b) { + warn!("ctrl_slash::set_enabled via sysfs: {err}"); + } config.enabled = enabled; config.brightness = brightness; @@ -95,18 +86,10 @@ impl SlashZbus { async fn set_brightness(&self, brightness: u8) { let mut config = self.0.lock_config().await; let enabled = brightness > 0; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - enabled, - brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + if let Err(err) = self.0.led().set_brightness(brightness) { + warn!("ctrl_slash::set_brightness via sysfs: {err}"); + } config.enabled = enabled; config.brightness = brightness; @@ -123,15 +106,10 @@ impl SlashZbus { #[zbus(property)] async fn set_interval(&self, interval: u8) { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, config.enabled, config.brightness, interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + if let Err(err) = self.0.led().set_slash_interval(interval) { + warn!("ctrl_slash::set_interval via sysfs: {err}"); + } config.display_interval = interval; config.write(); @@ -143,7 +121,7 @@ impl SlashZbus { Ok(config.display_mode as u8) } - /// Set interval between slash animations (0-255) + /// Set animation mode #[zbus(property)] async fn set_mode(&self, mode: u8) -> zbus::Result<()> { let mode = SlashMode::try_from(mode).map_err(|err| { @@ -151,12 +129,12 @@ impl SlashZbus { })?; let mut config = self.0.lock_config().await; - let command_packets = slash_pkt_set_mode(config.slash_type, mode); - // self.node.write_bytes(&command_packets[0])?; - self.0.write_bytes(&command_packets[1]).await?; self.0 - .write_bytes(&slash_pkt_save(config.slash_type)) - .await?; + .led() + .set_slash_mode(&mode.to_string()) + .map_err(|err| { + zbus::fdo::Error::Failed(format!("ctrl_slash::set_mode sysfs: {err}")) + })?; config.display_mode = mode; config.write(); @@ -164,7 +142,6 @@ impl SlashZbus { } /// Get the device state as stored by asusd - // #[zbus(property)] async fn device_state(&self) -> DeviceState { let config = self.0.lock_config().await; DeviceState::from(&*config) @@ -177,14 +154,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_boot(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_boot(config.slash_type, enable)) - .await?; - config.show_on_boot = enable; - config.write(); - Ok(()) + async fn set_show_on_boot(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -194,14 +165,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_sleep(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_sleep(config.slash_type, enable)) - .await?; - config.show_on_sleep = enable; - config.write(); - Ok(()) + async fn set_show_on_sleep(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -211,14 +176,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_shutdown(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_shutdown(config.slash_type, enable)) - .await?; - config.show_on_shutdown = enable; - config.write(); - Ok(()) + async fn set_show_on_shutdown(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -228,14 +187,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_battery(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_battery_saver(config.slash_type, enable)) - .await?; - config.show_on_battery = enable; - config.write(); - Ok(()) + async fn set_show_on_battery(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -245,14 +198,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_battery_warning(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_low_battery(config.slash_type, enable)) - .await?; - config.show_battery_warning = enable; - config.write(); - Ok(()) + async fn set_show_battery_warning(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -262,17 +209,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_lid_closed(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_lid_closed(config.slash_type, enable)) - .await?; - self.0 - .write_bytes(&slash_pkt_save(config.slash_type)) - .await?; - config.show_on_lid_closed = enable; - config.write(); - Ok(()) + async fn set_show_on_lid_closed(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } } @@ -280,40 +218,13 @@ impl Reloadable for SlashZbus { async fn reload(&mut self) -> Result<(), RogError> { debug!("reloading slash settings"); let config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("set_options {}", err); - }) - .ok(); - - macro_rules! write_bytes_with_warning { - ($packet_fn:expr, $cfg:ident, $warn_msg:expr) => { - self.0 - .write_bytes(&$packet_fn(config.slash_type, config.$cfg)) - .await - .map_err(|err| { - warn!("{} {}", $warn_msg, err); - }) - .ok(); - }; - } - write_bytes_with_warning!(slash_pkt_boot, show_on_boot, "show_on_boot"); - write_bytes_with_warning!(slash_pkt_sleep, show_on_sleep, "show_on_sleep"); - write_bytes_with_warning!(slash_pkt_shutdown, show_on_shutdown, "show_on_shutdown"); - write_bytes_with_warning!(slash_pkt_battery_saver, show_on_battery, "show_on_battery"); - write_bytes_with_warning!( - slash_pkt_low_battery, - show_battery_warning, - "show_battery_warning" - ); + let brightness = if config.enabled { config.brightness } else { 0 }; + self.0.led().set_brightness(brightness)?; + self.0.led().set_slash_interval(config.display_interval)?; + self.0 + .led() + .set_slash_mode(&config.display_mode.to_string())?; Ok(()) } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 823ee8214..598a69cf5 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -1,18 +1,18 @@ use std::sync::Arc; use config_traits::{StdConfig, StdConfigLoad}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use rog_anime::AnimeType; use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; use rog_aura::AuraDeviceType; use rog_platform::DynamicLed; +use rog_platform::SlashLed; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; use rog_scsi::{ScsiType, open_device}; use rog_slash::SlashType; -use rog_slash::error::SlashError; use tokio::sync::Mutex; use crate::aura_anime::AniMe; @@ -28,7 +28,6 @@ use crate::error::RogError; pub enum _DeviceHandle { /// The AniMe devices require USBRaw as they are not HID devices Usb(USBRaw), - HidRaw(HidRaw), LedClass(KeyboardBacklight), /// TODO MulticolourLed, @@ -48,57 +47,23 @@ pub enum DeviceHandle { } impl DeviceHandle { - /// Try Slash HID. If one exists it is initialised and returned. - pub async fn new_slash_hid( - device: Arc>, - prod_id: &str, - ) -> Result { - debug!("Testing for HIDRAW Slash"); - let slash_type = SlashType::from_dmi(); - if matches!(slash_type, SlashType::Unsupported) - || slash_type - .prod_id_str() - .to_lowercase() - .trim_start_matches("0x") - != prod_id - { - log::info!("Unknown or invalid slash: {prod_id:?}, skipping"); - return Err(RogError::NotFound("No slash device".to_string())); - } - info!("Found slash type {slash_type:?}: {prod_id}"); - + /// Try Slash sysfs LED. Presence of `asus::slash` is the capability gate; + /// board DMI lists are not used. + pub async fn maybe_slash() -> Result { + debug!("Testing for Slash"); + let led = SlashLed::new().map_err(|e| { + warn!("No Slash sysfs LED found: {e}"); + RogError::NotFound("No slash device found".to_string()) + })?; + + info!("Found Slash sysfs LED at {:?}", led.path()); let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new(Some(device), None, Arc::new(Mutex::new(config))); + config.slash_type = SlashType::Unsupported; + let slash = Slash::new(led, Arc::new(Mutex::new(config))); slash.do_initialization().await?; Ok(Self::Slash(slash)) } - /// Try Slash USB. If one exists it is initialised and returned. - pub async fn new_slash_usb() -> Result { - debug!("Testing for USB Slash"); - let slash_type = SlashType::from_dmi(); - if matches!(slash_type, SlashType::Unsupported) { - return Err(RogError::Slash(SlashError::NoDevice)); - } - - if let Ok(usb) = USBRaw::new(slash_type.prod_id()) { - info!("Found Slash USB {slash_type:?}"); - - let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new( - None, - Some(Arc::new(Mutex::new(usb))), - Arc::new(Mutex::new(config)), - ); - slash.do_initialization().await?; - Ok(Self::Slash(slash)) - } else { - Err(RogError::NotFound("No slash device found".to_string())) - } - } - pub async fn maybe_anime_usb() -> Result { debug!("Testing for USB AniMe"); let anime_type = get_anime_type(); diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index 7a4650cd4..8014a3358 100644 --- a/rog-platform/src/lib.rs +++ b/rog-platform/src/lib.rs @@ -13,6 +13,7 @@ pub mod keyboard_led; pub(crate) mod macros; pub mod platform; pub mod power; +pub mod slash_led; pub mod usb_raw; use std::path::Path; @@ -21,6 +22,7 @@ pub use dynamic_led::DynamicLed; use error::{PlatformError, Result}; use log::warn; use platform::PlatformProfile; +pub use slash_led::SlashLed; use udev::Device; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/rog-platform/src/slash_led.rs b/rog-platform/src/slash_led.rs new file mode 100644 index 000000000..98fb8aeba --- /dev/null +++ b/rog-platform/src/slash_led.rs @@ -0,0 +1,65 @@ +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Clone)] +pub struct SlashLed { + path: PathBuf, +} + +impl SlashLed { + attr_num!("brightness", path, u8); + attr_num!("max_brightness", path, u8); + + attr_string!("slash_mode", path); + attr_string!("slash_mode_index", path); + attr_num!("slash_interval", path, u8); + + pub fn new() -> Result { + let std_path = Path::new("/sys/class/leds/asus::slash"); + if std_path.exists() { + info!("Found Slash LED at {:?}", std_path); + return Ok(Self { + path: std_path.to_owned(), + }); + } + + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("{}", err); + PlatformError::Udev("enumerator failed".into(), err) + })?; + + enumerator.match_subsystem("leds").map_err(|err| { + warn!("{}", err); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("{}", err); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sys = device.sysname().to_string_lossy(); + if sys.contains("slash") { + info!("Found Slash LED controls at {:?}", device.sysname()); + return Ok(Self { + path: device.syspath().to_owned(), + }); + } + } + + Err(PlatformError::MissingFunction( + "SlashLed:new(), asus::slash not found".into(), + )) + } + + pub fn is_available() -> bool { + Self::new().is_ok() + } + + pub fn path(&self) -> &Path { + &self.path + } +} diff --git a/rog-slash/Cargo.toml b/rog-slash/Cargo.toml index 1b3dfe27b..daf39b471 100644 --- a/rog-slash/Cargo.toml +++ b/rog-slash/Cargo.toml @@ -13,9 +13,8 @@ keywords = ["ROG", "ASUS", "AniMe", "Slash"] exclude = ["data"] [features] -default = ["dbus", "detect"] +default = ["dbus"] dbus = ["zbus"] -detect = ["dmi_id"] [lib] name = "rog_slash" @@ -24,5 +23,4 @@ path = "src/lib.rs" [dependencies] serde.workspace = true zbus = { workspace = true, optional = true } -dmi_id = { path = "../dmi-id", optional = true } thiserror.workspace = true diff --git a/rog-slash/src/data.rs b/rog-slash/src/data.rs index 8cd0a50e6..a2319aca3 100644 --- a/rog-slash/src/data.rs +++ b/rog-slash/src/data.rs @@ -1,7 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use dmi_id::DMIID; use serde::{Deserialize, Serialize}; #[cfg(feature = "dbus")] use zbus::zvariant::Type; @@ -56,33 +55,13 @@ impl SlashType { } } - pub fn from_dmi() -> Self { - let board_name = DMIID::new().unwrap_or_default().board_name.to_uppercase(); - if board_name.contains("G614F") { - SlashType::G614_2025 - } else if [ - "GA403W", "GA403UH", "GA403UM", "GA403UP", "GA403GM", - ] - .iter() - .any(|s| board_name.contains(s)) - { - SlashType::GA403_2025 - } else if board_name.contains("GA403") { - SlashType::GA403_2024 - } else if board_name.contains("GA605K") { - SlashType::GA605_2025 - } else if board_name.contains("GA605") { - SlashType::GA605_2024 - } else if board_name.contains("GU405") { - SlashType::GU405_2026 - } else if board_name.contains("GU606") { - SlashType::GU606_2026 - } else if board_name.contains("GU605C") { - SlashType::GU605_2025 - } else if board_name.contains("GU605") { - SlashType::GU605_2024 - } else { - SlashType::Unsupported + /// Select hidraw packet layout from the USB product id. + /// Report 0x5e vs 0x5d is a device capability, not a DMI board name. + pub fn from_usb_product(id: &str) -> Self { + match id.trim_start_matches("0x").to_ascii_uppercase().as_str() { + PROD_ID1_STR => SlashType::GA403_2024, + PROD_ID2_STR => SlashType::GA403_2025, + _ => SlashType::Unsupported, } } } From a2e293d38bc5ab044154f9251c28dd63d4d1106e Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 16 Oct 2026 11:14:44 +0200 Subject: [PATCH 9/9] fix(asusd): restore Aura lighting after s2idle resume AuraZbus::start_tasks never registered CtrlTask sleep handlers, so keyboard/lightbar effects were not reapplied after suspend. Wire create_tasks and re-apply power states, brightness, and the current mode after resume with a short settle/retry for Dynamic Lighting. Signed-off-by: Marco Scardovi --- asusd/src/aura_laptop/trait_impls.rs | 66 ++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index f8561d161..797cfc91d 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -32,6 +32,20 @@ impl AuraZbus { self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); + + // Wire logind sleep/resume (and related) handlers. Without this, Aura never + // re-applies power states / effects after s2idle and keyboard LEDs stay off. + match SignalEmitter::new(connection, path.as_ref()).map(|s| s.to_owned()) { + Ok(signal) => { + if let Err(e) = self.create_tasks(signal).await { + warn!("Aura CtrlTask (sleep/resume) setup failed: {e}"); + } + } + Err(e) => { + warn!("Aura could not create SignalEmitter for CtrlTask: {e}"); + } + } + connection .object_server() .at(path.clone(), self) @@ -280,27 +294,22 @@ impl CtrlTask for AuraZbus { self.create_sys_event_tasks( move |sleeping| { let inner1 = inner1.clone(); - // unwrap as we want to bomb out of the task async move { - if !sleeping { - info!("CtrlKbdLedTask reloading brightness and modes"); - let brightness = inner1.config.lock().await.brightness; - if let Err(e) = inner1.set_brightness(brightness).await { - error!("CtrlKbdLedTask: {e}"); - } - let mut config = inner1.config.lock().await; - if let Err(e) = inner1.write_current_config_mode(&mut config).await { - error!("CtrlKbdLedTask: {e}"); + if sleeping { + if let Err(e) = inner1.update_config().await { + error!("CtrlKbdLedTask: persist config before sleep: {e}"); } - } else if let Err(e) = inner1.update_config().await { - error!("CtrlKbdLedTask: {e}"); + return; } + + // Dynamic Lighting / HID nodes can lag behind s2idle resume. + restore_aura_after_resume(&inner1).await; } }, move |_shutting_down| { let inner3 = inner3.clone(); async move { - info!("CtrlKbdLedTask reloading brightness and modes"); + info!("CtrlKbdLedTask restoring brightness on shutdown path"); let brightness = inner3.config.lock().await.brightness; if let Err(e) = inner3.set_brightness(brightness).await { error!("CtrlKbdLedTask: {e}"); @@ -322,6 +331,37 @@ impl CtrlTask for AuraZbus { } } +/// Re-apply power states, brightness, and the current effect after resume. +/// +/// Matches [`Reloadable::reload`] ordering, with a short settle delay and one +/// retry so Dynamic Lighting sysfs is ready after s2idle. +async fn restore_aura_after_resume(ctrl: &Aura) { + info!("CtrlKbdLedTask restoring Aura after resume"); + const SETTLE_MS: u64 = 500; + for attempt in 1..=2u8 { + tokio::time::sleep(std::time::Duration::from_millis(SETTLE_MS)).await; + + let mut config = ctrl.config.lock().await; + if let Err(e) = ctrl.set_power_states(&config).await { + // Missing power_states on DL is expected; still try brightness/mode. + warn!("CtrlKbdLedTask: restore power states (attempt {attempt}): {e}"); + } + let brightness = config.brightness; + if let Err(e) = ctrl.set_brightness(brightness).await { + warn!("CtrlKbdLedTask: restore brightness (attempt {attempt}): {e}"); + } + match ctrl.write_current_config_mode(&mut config).await { + Ok(()) => { + info!("CtrlKbdLedTask: Aura restored after resume"); + return; + } + Err(e) => { + error!("CtrlKbdLedTask: restore mode (attempt {attempt}): {e}"); + } + } + } +} + impl Reloadable for AuraZbus { async fn reload(&mut self) -> Result<(), RogError> { self.0.fix_ally_power().await?;