From cab7684326477822af981c3133c81bed068d8b0c Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:55:47 +0200 Subject: [PATCH 1/6] 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. --- 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 2dd5563e42e5e24368f776d6b451c7360f76bdf9 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:56:30 +0200 Subject: [PATCH 2/6] 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. Keep ASUS aura_mode separate from generic node validation. The unused optional frame sink remains deliberately unexposed. Signed-off-by: Marco Scardovi --- rog-platform/src/dynamic_led.rs | 304 ++++++++++++++++++++++++++++++++ rog-platform/src/lib.rs | 16 +- 2 files changed, 314 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..aa46cd42a --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,304 @@ +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_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. + /// `"aura: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. "aura:keyboard" or "asus::kbd_backlight"). + 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()) + } + + /// 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) + } + + /// Whether this ASUS node exposes the non-generic topology selector. + pub fn has_asus_aura_mode(&self) -> bool { + self.name().starts_with("aura:") && self.path.join("aura_mode").exists() + } + + /// Read the active ASUS topology mode. + pub fn get_asus_aura_mode(&self) -> Result { + parse_active_index( + &std::fs::read_to_string(self.path.join("aura_mode")).map_err(|e| { + PlatformError::IoPath(self.path.join("aura_mode").display().to_string(), e) + })?, + ) + } + + /// Set the ASUS topology mode (`auto`, `unified`, or `split`). + pub fn set_asus_aura_mode(&self, mode: &str) -> Result<()> { + if !matches!(mode, "auto" | "unified" | "split") { + return Err(PlatformError::InvalidValue); + } + let path = self.path.join("aura_mode"); + std::fs::write(&path, mode) + .map_err(|e| PlatformError::IoPath(path.display().to_string(), e)) + } +} + +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_active_index(raw: &str) -> Result { + raw.split_whitespace() + .find_map(|word| word.strip_prefix('[')?.strip_suffix(']')) + .map(str::to_owned) + .ok_or(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_parse_active_aura_mode() { + let sample = "[auto] unified split\n"; + assert_eq!(super::parse_active_index(sample).unwrap(), "auto"); + + let sample2 = "auto [unified] split\n"; + assert_eq!(super::parse_active_index(sample2).unwrap(), "unified"); + assert!(super::parse_active_index("auto unified split").is_err()); + } + + #[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 096fa1aefc58e68e6d30579249837f2751f0bd41 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:56:37 +0200 Subject: [PATCH 3/6] 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 current kernel effect_index: static, breathe, rainbow_cycle, rainbow_wave, and direct. direct is AuraModeNum::Direct. Signed-off-by: Marco Scardovi --- rog-aura/src/builtin_modes.rs | 187 +++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..247c30ff9 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,42 @@ 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. + /// + /// Matches the current kernel `effect_index`: `static`, `breathe`, + /// `rainbow_cycle`, `rainbow_wave`, `direct`. + 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::Direct => Some("direct"), + _ => None, + } + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +506,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 +571,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 +651,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 +820,56 @@ 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(), None); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), None); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), None); + 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 146232471843bfac4e63c184e5aef68f669ff02b Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:35:15 +0200 Subject: [PATCH 4/6] 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. Clean up dead hidraw/comment leftovers in anime and device handles, stream exact led_count*3 RGB payloads through direct_buffer, and honour ASUS aura_mode topology: skip -EBUSY on inactive unified/split nodes, map legacy Key1–4 / bar zones onto keyboard/lightbar nodes, 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 | 617 +++++++++++++++++++++++---- asusd/src/aura_laptop/trait_impls.rs | 168 ++++---- asusd/src/aura_manager.rs | 72 +--- asusd/src/aura_types.rs | 67 +-- docs/SUMMARY.md | 1 + docs/usage/dynamic-lighting.md | 55 +++ rog-platform/src/hid_raw.rs | 81 +++- 8 files changed, 802 insertions(+), 278 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 f6ebef2ca..9d2ff043d 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 log::{debug, info}; use rog_aura::keyboard::{AuraLaptopUsbPackets, 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,14 +19,65 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Aura { + pub dynamic_global: Option>>, + 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_global.is_some() + || self.dynamic_lightbar.is_some() + } + + /// `aura:global`, then keyboard, then lightbar. Callers that need a + /// different order keep their own list. + fn dynamic_nodes(&self) -> [Option<&Arc>>; 3] { + [ + self.dynamic_global.as_ref(), + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + ] + } + + /// Node whose `effect_index` describes the basic modes. Unified topology + /// advertises them on `aura:global`; otherwise the keyboard node does. + async fn effect_source(&self) -> Option> { + if let Some(global) = &self.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + } + } + + async fn set_asus_topology(&self, mode: &str) -> Result<(), RogError> { + for led in self.dynamic_nodes().into_iter().flatten() { + let led = led.lock().await; + if led.has_asus_aura_mode() { + led.set_asus_aura_mode(mode)?; + return Ok(()); + } + } + Err(RogError::MissingFunction( + "ASUS Dynamic Lighting topology control is unavailable".into(), + )) + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { + // With a chassis lightbar, prefer split so keyboard and lightbar are + // independently writable (matches kernel auto→split). Unified remains + // available when userspace wants a single global effect. + if self.dynamic_lightbar.is_some() { + self.set_asus_topology("split").await?; + } Ok(()) } @@ -35,14 +88,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,82 +143,301 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { - if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { - if let Some(platform) = &self.backlight { - let buf = [ - 1, mode.mode as u8, mode.colour1.r, mode.colour1.g, mode.colour1.b, - mode.speed as u8, - ]; - platform.lock().await.set_kbd_rgb_mode(&buf)?; + // 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); + } + 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 => { + self.set_asus_topology("split").await?; + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led)? { + return Ok(()); + } + } + } + rog_aura::AuraZone::None => { + // Whole-device: only unified topology uses aura:global. + // Otherwise (split / auto→split) fan out to keyboard and + // lightbar so both zones get the same effect. + let unified = if let Some(global) = &self.dynamic_global { + let led = global.lock().await; + led.has_asus_aura_mode() + && led.get_asus_aura_mode().ok().as_deref() == Some("unified") + } else { + false + }; + if unified && let Some(global) = &self.dynamic_global { + let global_led = global.lock().await; + if apply_to_led(&global_led)? { + return Ok(()); + } + } + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + let mut 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 self.dynamic_lightbar.is_some() { + self.set_asus_topology("split").await?; + } + 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 + { + let buf = [ + 1, mode.mode as u8, mode.colour1.r, mode.colour1.g, mode.colour1.b, + mode.speed as u8, + ]; + platform.lock().await.set_kbd_rgb_mode(&buf)?; + 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) + } + + /// Nodes that hold live brightness for the current ASUS topology. + /// Kernel `auto` resolves to split, so keyboard/lightbar are canonical. + async fn topology_brightness_slots(&self) -> [Option<&Arc>>; 3] { + let mut mode = None; + for slot in self.dynamic_nodes().into_iter().flatten() { + let led = slot.lock().await; + if led.has_asus_aura_mode() { + mode = led.get_asus_aura_mode().ok(); + break; + } + } + match mode.as_deref() { + Some("unified") => [ + self.dynamic_global.as_ref(), + None, + None, + ], + Some("split") | Some("auto") => [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + None, + ], + _ => [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + self.dynamic_global.as_ref(), + ], + } } - pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + /// Read the current brightness as a 4-step [`LedBrightness`] level. + /// + /// Dynamic Lighting nodes may advertise `max_brightness > 3`; those values + /// are scaled back to Off/Low/Med/High. LED class reads do not return + /// `-EBUSY`, so inactive nodes can still look readable — follow `aura_mode` + /// (kernel `auto` is split) instead of trusting the first node that replies. + pub async fn get_brightness(&self) -> Result { + for slot in self.topology_brightness_slots().await.into_iter().flatten() { + let led = slot.lock().await; + 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. + /// + /// When Dynamic Lighting nodes are present the value is scaled to each + /// *active* node's `max_brightness`. Under ASUS `aura_mode=unified` only + /// `aura:global` accepts writes (`keyboard`/`lightbar` return `-EBUSY`); + /// under `split` the reverse is true. Inactive-node errors are skipped so + /// Fn-key brightness works in either topology. The legacy + /// `KeyboardBacklight` (0..=3) is also updated when present. + pub async fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + let mut applied = false; + for led in self.dynamic_nodes().into_iter().flatten() { + let led = led.lock().await; + 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 { - // TODO: tuf bool array - let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; + if self.has_dynamic_lighting() { + let mut requested = Vec::new(); + for state in &config.enabled.states { + if state.boot { + requested.push("boot"); + } + if state.awake { + requested.push("awake"); + } + if state.sleep { + requested.push("sleep"); + } + if state.shutdown { + requested.push("shutdown"); + } + } + let mut applied = false; + let mut saw_power_states = false; + let mut unsupported_request = false; + for led in self.dynamic_nodes().into_iter().flatten() { + 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(()) } @@ -186,56 +455,242 @@ 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(()); + } + + let dynamic_leds = [ + self.dynamic_kbd.as_ref(), + self.dynamic_global.as_ref(), + ]; + for maybe_led in dynamic_leds.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() { + 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 returns `-EBUSY` when writing a Dynamic Lighting node that is inactive +/// for the current `aura_mode` (`unified` vs `split`). +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)) +} + +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() +} + +#[cfg(test)] +mod tests { + use std::io::{Error, ErrorKind}; + + use rog_aura::AuraZone; + use rog_platform::error::PlatformError; + + use super::{ + direct_rgb_payload, filter_power_states, is_dl_node_inactive, 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 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( + "aura:keyboard/brightness".into(), + Error::new(ErrorKind::ResourceBusy, "Device or resource busy"), + )); + assert!(is_dl_node_inactive(&err)); + let errno = RogError::Platform(PlatformError::IoPath( + "aura: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..0c8f01872 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,10 +167,41 @@ impl DeviceHandle { Some(Arc::new(Mutex::new(k))) }); + // Check for Dynamic Lighting interface + let (dynamic_global, dynamic_kbd, dynamic_lightbar) = { + let global = DynamicLed::find("aura:global") + .map(|g| { + info!("Dynamic Lighting global aggregate detected: aura:global"); + Arc::new(Mutex::new(g)) + }) + .ok(); + let kbd = DynamicLed::find("aura:keyboard") + .map(|k| { + info!("Dynamic Lighting keyboard detected: aura:keyboard"); + Arc::new(Mutex::new(k)) + }) + .ok(); + let lb = DynamicLed::find("aura:lightbar") + .map(|l| { + info!("Dynamic Lighting lightbar detected: aura:lightbar"); + Arc::new(Mutex::new(l)) + }) + .ok(); + if global.is_some() || kbd.is_some() || lb.is_some() { + (global, kbd, lb) + } else { + debug!("Dynamic Lighting not detected; using legacy hidraw fallback"); + (None, None, None) + } + }; + // 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 { + dynamic_global, + dynamic_kbd, + dynamic_lightbar, hid: device, 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..362204869 --- /dev/null +++ b/docs/usage/dynamic-lighting.md @@ -0,0 +1,55 @@ +# 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, 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 (`aura_mode`) + +The ASUS-only `aura_mode` attribute selects kernel topology; it is not part of +the generic Dynamic Lighting ABI. + +| Mode | Writable nodes | Notes | +|------|----------------|-------| +| `auto` | same as `split` | Kernel default resolution | +| `split` | `aura:keyboard`, `aura:lightbar` | Independent colours; `aura:global` returns `-EBUSY` | +| `unified` | `aura:global` | Single effect for all zones; split nodes return `-EBUSY` | + +When a chassis lightbar is present, asusd sets `split` during initialization so +keyboard and lightbar are independently controllable. Callers that want one +shared effect should set `unified` explicitly. + +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 under split or uses `aura:global` under unified. + +## 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 7e08325ce55a4bd6c5ec4d7a7899bee7da9881b2 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 25 Sep 2026 14:43:31 +0200 Subject: [PATCH 5/6] 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 26d1aec03b16a6fc7ee62757908bad4c6012c5e8 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Sat, 12 Sep 2026 19:13:14 +0200 Subject: [PATCH 6/6] 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 | 110 +++++----------- 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, 181 insertions(+), 341 deletions(-) create mode 100644 rog-platform/src/slash_led.rs diff --git a/Cargo.lock b/Cargo.lock index a8b228b49..047f0c9c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4816,7 +4816,6 @@ dependencies = [ name = "rog_slash" version = "6.5.0" dependencies = [ - "dmi_id", "serde", "thiserror 2.0.20", "zbus", diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index 7cebf5caf..d982fe70f 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,36 +148,22 @@ 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 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_key = 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.clone() { let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); @@ -214,22 +179,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 +227,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 +367,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 +390,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 +471,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 +509,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 +626,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 +648,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 0c8f01872..41bc9b0a1 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, } } }