From 2b70bd6daf9047dc3cb62fd9aa921dee574022c0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:55:47 +0200 Subject: [PATCH 01/12] 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 365ada8d712a84f039b797e2c608f137e6085a97 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:56:30 +0200 Subject: [PATCH 02/12] feat(rog-platform): add dynamic lighting led sysfs abstraction Introduce DynamicLed for communicating with the Linux kernel Dynamic Lighting class interface under /sys/class/leds//. This wrapper provides methods to inspect and control: - effect and dynamically queried supported effects via effect_index - direction and direction_index - speed and speed_range - effects_palette using space-separated #RRGGBB values - write_direct for streaming raw binary frame data - brightness, max_brightness, and power_states attributes --- rog-platform/src/dynamic_led.rs | 137 ++++++++++++++++++++++++++++++++ rog-platform/src/lib.rs | 2 + 2 files changed, 139 insertions(+) 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..9ca03568c --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,137 @@ +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 { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: device.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) + } + + /// Check if a dynamic LED is present on the system. + pub fn is_available(name: &str) -> bool { + Self::find(name).is_ok() + } + + /// Return the sysfs path. + pub fn path(&self) -> &Path { + &self.path + } + + /// 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 if a given effect mode string is supported by the kernel driver. + pub fn is_effect_supported(&self, effect: &str) -> bool { + self.get_supported_effects_list() + .map(|list| list.iter().any(|e| e == effect)) + .unwrap_or(false) + } + + /// 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()) + } + + /// Write raw RGB byte buffer directly to the `direct_buffer` (or `direct`) binary attribute. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + let direct_path = if self.path.join("direct_buffer").exists() { + self.path.join("direct_buffer") + } else { + self.path.join("direct") + }; + std::fs::write(&direct_path, 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<()> { + 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) + } +} + +#[cfg(test)] +mod tests { + + #[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"); + } +} diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index dec0bb861..cecdd941f 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; From 5d59a5f57e323e1eac64a0715776c9e4cc9394c0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:56:43 +0200 Subject: [PATCH 03/12] 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, from_dynamic_effect_str} - Speed::{to_dynamic_speed, from_dynamic_speed} - Direction::{to_dynamic_direction_str, from_dynamic_direction_str} - AuraEffect::to_dynamic_palette for extracting RGB palette tuples --- rog-aura/src/builtin_modes.rs | 160 +++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..e6ab38c54 100644 --- a/rog-aura/src/builtin_modes.rs +++ b/rog-aura/src/builtin_modes.rs @@ -199,6 +199,24 @@ 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, + } + } + + pub const fn from_dynamic_speed(val: u32) -> Self { + match val { + 0 => Self::Low, + 2 => Self::High, + _ => Self::Med, + } + } +} /// Used for Rainbow mode. /// /// Enum corresponds to the required integer value @@ -248,6 +266,26 @@ 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", + } + } + + pub fn from_dynamic_direction_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "left" => Self::Left, + "up" => Self::Up, + "down" => Self::Down, + _ => Self::Right, + } + } +} + /// Enum of modes that convert to the actual number required by a USB HID packet #[cfg_attr( feature = "dbus", @@ -359,6 +397,32 @@ impl From for AuraModeNum { } } +impl AuraModeNum { + /// Return the corresponding Dynamic Lighting effect name, if available. + pub const fn to_dynamic_effect_str(&self) -> Option<&'static str> { + match self { + Self::Static => Some("static"), + Self::Breathe => Some("breathing"), + Self::RainbowCycle => Some("spectrum_cycle"), + Self::RainbowWave => Some("rainbow"), + Self::Pulse | Self::Flash => Some("strobe"), + _ => None, + } + } + + /// Parse a Dynamic Lighting effect name into an `AuraModeNum`. + pub fn from_dynamic_effect_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "static" => Some(Self::Static), + "breathing" => Some(Self::Breathe), + "spectrum_cycle" => Some(Self::RainbowCycle), + "rainbow" => Some(Self::RainbowWave), + "strobe" => Some(Self::Pulse), + _ => None, + } + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +468,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 +533,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 { @@ -689,4 +763,88 @@ 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("breathing") + ); + assert_eq!( + AuraModeNum::RainbowCycle.to_dynamic_effect_str(), + Some("spectrum_cycle") + ); + assert_eq!( + AuraModeNum::RainbowWave.to_dynamic_effect_str(), + Some("rainbow") + ); + assert_eq!(AuraModeNum::Pulse.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), None); + + assert_eq!( + AuraModeNum::from_dynamic_effect_str("static"), + Some(AuraModeNum::Static) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("breathing"), + Some(AuraModeNum::Breathe) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("spectrum_cycle"), + Some(AuraModeNum::RainbowCycle) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("rainbow"), + Some(AuraModeNum::RainbowWave) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("strobe"), + Some(AuraModeNum::Pulse) + ); + assert_eq!(AuraModeNum::from_dynamic_effect_str("unknown"), None); + + 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!(Speed::from_dynamic_speed(0), Speed::Low); + assert_eq!(Speed::from_dynamic_speed(1), Speed::Med); + assert_eq!(Speed::from_dynamic_speed(2), Speed::High); + + 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"); + assert_eq!( + Direction::from_dynamic_direction_str("left"), + Direction::Left + ); + assert_eq!( + Direction::from_dynamic_direction_str("right"), + Direction::Right + ); + + 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 77d208f7b52ef13e58592d482fc3ce736cc52316 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 16:57:14 +0200 Subject: [PATCH 04/12] feat(asusd): prioritize dynamic lighting interface with hidraw fallback Probe for /sys/class/leds/aura:global, aura:keyboard, and aura:lightbar Dynamic Lighting LED devices during Aura laptop initialization. When present: - Query available hardware effect modes dynamically from the kernel via sysfs effect_index instead of hardcoding assumptions. - Route global laptop requests (AuraZone::None) directly to aura:global for aggregate hardware execution on AURA_ZONE_ALL. - Route individual zone requests to aura:keyboard or aura:lightbar for independent zone control. - Prioritize the sysfs Dynamic Lighting interface for effect application, brightness control, and power states. - Retain the legacy USB hidraw device handle and fallback to legacy packets if an effect is unsupported by Dynamic Lighting or on older kernels. --- asusd/src/aura_laptop/mod.rs | 145 ++++++++++++++++++++++++++- asusd/src/aura_laptop/trait_impls.rs | 88 +++++++++++++++- asusd/src/aura_types.rs | 32 ++++++ 3 files changed, 259 insertions(+), 6 deletions(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index f6ebef2ca..964c6d628 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -3,9 +3,11 @@ use std::sync::Arc; use config::AuraConfig; use config_traits::StdConfig; use log::info; +use log::{debug, warn}; 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::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -17,12 +19,20 @@ 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() + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { Ok(()) @@ -36,7 +46,11 @@ impl Aura { /// this in scope then a deadlock can occur. pub async fn update_config(&self) -> Result<(), RogError> { let mut config = self.config.lock().await; - let bright = if let Some(bl) = self.backlight.as_ref() { + let bright = if let Some(dynamic) = self.dynamic_global.as_ref() { + dynamic.lock().await.get_brightness().unwrap_or_default() + } else if let Some(dynamic) = self.dynamic_kbd.as_ref() { + dynamic.lock().await.get_brightness().unwrap_or_default() + } else if let Some(bl) = self.backlight.as_ref() { bl.lock().await.get_brightness().unwrap_or_default() } else { config.brightness.into() @@ -93,6 +107,90 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { + // Priority: Dynamic Lighting sysfs interface + 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| -> bool { + if led.is_effect_supported(eff_str) { + let _ = led.set_speed(speed); + let _ = led.set_direction(dir_str); + let _ = led.set_palette_colors(&palette); + if let Err(e) = led.set_effect(eff_str) { + warn!("Failed to set dynamic lighting effect '{eff_str}': {e}"); + false + } else { + true + } + } else { + debug!( + "Dynamic lighting effect '{eff_str}' not supported by kernel, trying fallback" + ); + false + } + }; + + match mode.zone { + rog_aura::AuraZone::BarLeft | rog_aura::AuraZone::BarRight => { + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led) { + return Ok(()); + } + } else { + debug!( + "Skipping unsupported lightbar zone in dynamic lighting unified mode" + ); + return Ok(()); + } + } + rog_aura::AuraZone::None => { + if 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 kbd_ok = apply_to_led(&kbd_led); + if let Some(lb) = &self.dynamic_lightbar { + let lb_led = lb.lock().await; + let _ = apply_to_led(&lb_led); + } + if kbd_ok { + return Ok(()); + } + } + } + _ => { + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + if apply_to_led(&kbd_led) { + return Ok(()); + } + } else if let Some(global) = &self.dynamic_global { + let global_led = global.lock().await; + if apply_to_led(&global_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: TUF platform or legacy hidraw if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { if let Some(platform) = &self.backlight { let buf = [ @@ -128,6 +226,24 @@ impl Aura { } pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + let mut updated = false; + if let Some(dynamic_global) = &self.dynamic_global + && dynamic_global.lock().await.set_brightness(value).is_ok() + { + updated = true; + } + if let Some(dynamic_kbd) = &self.dynamic_kbd + && dynamic_kbd.lock().await.set_brightness(value).is_ok() + { + updated = true; + } + if let Some(dynamic_lb) = &self.dynamic_lightbar { + let _ = dynamic_lb.lock().await.set_brightness(value); + } + if updated { + return Ok(()); + } + if let Some(backlight) = &self.backlight { backlight.lock().await.set_brightness(value)?; return Ok(()); @@ -140,6 +256,33 @@ impl Aura { /// 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 let Some(dynamic_led) = self.dynamic_global.as_ref().or(self.dynamic_kbd.as_ref()) { + let kbd = dynamic_led.lock().await; + if kbd.has_power_states() { + let mut states = Vec::new(); + for state in &config.enabled.states { + if state.boot { + states.push("boot"); + } + if state.awake { + states.push("awake"); + } + if state.sleep { + states.push("sleep"); + } + if state.shutdown { + states.push("shutdown"); + } + } + let states_str = states.join(" "); + if let Err(e) = kbd.set_power_states(&states_str) { + warn!("Failed to set power states via dynamic lighting: {e}"); + } else { + return Ok(()); + } + } + } + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { if let Some(backlight) = &self.backlight { // TODO: tuf bool array diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 90629044d..79270e4c8 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -97,13 +97,62 @@ 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 led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + if let Some(led) = led_lock { + let mut modes = Vec::new(); + for mode in config.builtins.keys() { + if let Some(eff_str) = mode.to_dynamic_effect_str() + && led.is_effect_supported(eff_str) + { + modes.push(*mode); + } + } + return Ok(modes); + } + } Ok(config.builtins.keys().cloned().collect()) } #[zbus(property)] async fn supported_basic_zones(&self) -> Result, ZbErr> { let config = self.0.config.lock().await; - Ok(config.support_data.basic_zones.clone()) + if self.0.has_dynamic_lighting() { + // If the kernel exposes only aura:global (unified chassis), there are + // no independent basic zones; the entire lighting is driven as a single unit. + if self.0.dynamic_kbd.is_none() && self.0.dynamic_lightbar.is_none() { + return Ok(vec![]); + } + let mut zones = config.support_data.basic_zones.clone(); + if self.0.dynamic_lightbar.is_none() { + zones.retain(|z| !matches!(z, AuraZone::BarLeft | AuraZone::BarRight)); + } else { + if !zones.contains(&AuraZone::BarLeft) { + zones.push(AuraZone::BarLeft); + } + if !zones.contains(&AuraZone::BarRight) { + zones.push(AuraZone::BarRight); + } + } + Ok(zones) + } else { + let mut zones = config.support_data.basic_zones.clone(); + if self.0.dynamic_lightbar.is_some() { + if !zones.contains(&AuraZone::BarLeft) { + zones.push(AuraZone::BarLeft); + } + if !zones.contains(&AuraZone::BarRight) { + zones.push(AuraZone::BarRight); + } + } + Ok(zones) + } } #[zbus(property)] @@ -166,10 +215,39 @@ 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() { + let led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + led_lock.is_some_and(|l| l.is_effect_supported(eff_str)) + } else { + false + }; + let zone_ok = match effect.zone { + AuraZone::None => true, + AuraZone::BarLeft | AuraZone::BarRight => self.0.dynamic_lightbar.is_some(), + _ => { + self.0.dynamic_kbd.is_some() + && config.support_data.basic_zones.contains(&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:?}" ))); diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index c31de89a0..736bc73b2 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; @@ -197,10 +198,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)), From db7cbc670a55802a5b08c3c435df0a55292ffa4b Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Tue, 8 Sep 2026 20:30:40 +0200 Subject: [PATCH 05/12] refactor(asusd): remove commented dead code in maybe_anime_hid Remove legacy commented-out HIDRAW AniMe Matrix probe code in DeviceHandle::maybe_anime_hid. AniMe Matrix over HIDRAW is currently unsupported and immediately returns an explicit NotFound error, making the commented lines dead code. --- asusd/src/aura_types.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 736bc73b2..dcd5b2038 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -112,21 +112,6 @@ impl DeviceHandle { 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 { From a58a9d420fe079067fdd8cb112f2f690452264c0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:08:47 +0200 Subject: [PATCH 06/12] refactor(asusd): remove hidraw fallback and legacy packet handling from aura laptop With the introduction of the Dynamic Lighting sysfs interface (/sys/class/leds/aura:*), direct USB HID report 0x5d writes via hidraw are no longer necessary for Aura laptop keyboard, lightbar, or unified chassis control. - Remove the `hid` (HidRaw) handle from the `Aura` struct. - Remove raw 64-byte padded packet writes (AURA_LAPTOP_LED_SET/APPLY) and fallback paths from `write_effect_and_apply`, `set_power_states`, `write_effect_block`, and `fix_ally_power`. - Update `DeviceHandle::maybe_laptop_aura` to not require a HidRaw handle. - In `aura_manager`, bypass opening `/dev/hidrawX` for Aura laptop keyboards to avoid unnecessary open file descriptors and kernel hidraw minor leaks. --- asusd/src/aura_laptop/mod.rs | 120 ++++++----------------------------- asusd/src/aura_manager.rs | 66 ++++++++++--------- asusd/src/aura_types.rs | 24 +++---- 3 files changed, 70 insertions(+), 140 deletions(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 964c6d628..8f7f8c8c7 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -4,11 +4,9 @@ use config::AuraConfig; use config_traits::StdConfig; use log::info; use log::{debug, warn}; -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_aura::keyboard::AuraLaptopUsbPackets; +use rog_aura::{AuraDeviceType, AuraEffect, LedBrightness}; use rog_platform::DynamicLed; -use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -22,7 +20,6 @@ pub struct Aura { pub dynamic_global: Option>>, pub dynamic_kbd: Option>>, pub dynamic_lightbar: Option>>, - pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, } @@ -190,39 +187,19 @@ impl Aura { )); } - // Fallback: TUF platform or legacy hidraw - 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)?; - } - } 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); + // Fallback: TUF platform sysfs backlight + 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(()); } - Ok(()) + Err(RogError::NoAuraKeyboard) } pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { @@ -283,35 +260,12 @@ impl Aura { } } - 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)?; - } - } 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 = [ - 0x5d, - 0xd1, - 0x09, - 0x01, - p.new_to_byte() as u8, - 0x0, - 0x0, - ]; - hid_raw.write_bytes(&msg)?; - return Ok(()); - } - - let bytes = config.enabled.to_bytes(config.led_type); - let msg = [ - 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], - ]; - hid_raw.write_bytes(&msg)?; + 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)?; } Ok(()) } @@ -329,27 +283,7 @@ impl Aura { config.write(); } - let pkt_type = effect[0][1]; - const PER_KEY_TYPE: u8 = 0xbc; - - if let Some(hid_raw) = &self.hid { - let hid_raw = hid_raw.lock().await; - if pkt_type != PER_KEY_TYPE { - config.per_key_mode_active = false; - hid_raw.write_bytes(&effect[0])?; - 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)?; - config.per_key_mode_active = true; - } - for row in effect.iter() { - hid_raw.write_bytes(row)?; - } - } - } else if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) && let Some(tuf) = &self.backlight { for row in effect.iter() { @@ -365,20 +299,6 @@ impl Aura { } 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 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.write(); - } Ok(()) } } diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d809bd744..744ec483e 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use dmi_id::DMIID; 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; @@ -170,45 +171,54 @@ 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 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 + ) { + // AURA LAPTOP DEVICE - driven by Dynamic Lighting / sysfs, no hidraw needed + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(usb_id_str).await + && let DeviceHandle::Aura(aura) = dev_type.clone() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); - let ctrl = SlashZbus::new(slash); + let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); + let ctrl = AuraZbus::new(aura); if ctrl .start_tasks(connection, path.clone()) .await .map_err(|e| { - error!("Failed to start Slash tasks: {e:?}, not adding this device") + error!("Failed to start Aura tasks: {e:?}, not adding this device") }) .is_ok() { devices.push(AsusDevice { device: dev_type, dbus_path: path, - hid_key: Some(hid_key.clone()), + hid_key: None, }); } } - // 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() + return Ok(devices); + } + + // For other devices that still require a shared hid handle (e.g. Slash): + // 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_str).await + && let DeviceHandle::Slash(slash) = dev_type.clone() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_anime()); - let ctrl = AniMeZbus::new(anime); + 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 AniMe tasks: {e:?}, not adding this device") + error!("Failed to start Slash tasks: {e:?}, not adding this device") }) .is_ok() { @@ -219,19 +229,17 @@ impl DeviceManager { }); } } - // AURA LAPTOP DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Aura(aura) = dev_type.clone() + // ANIME MATRIX DEVICE + if let Ok(dev_type) = DeviceHandle::maybe_anime_hid(dev.clone(), usb_id_str).await + && let DeviceHandle::AniMe(anime) = dev_type.clone() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); - let ctrl = AuraZbus::new(aura); + 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 Aura tasks: {e:?}, not adding this device") + error!("Failed to start AniMe tasks: {e:?}, not adding this device") }) .is_ok() { @@ -515,7 +523,7 @@ impl DeviceManager { ); 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 + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura("tuf").await && let DeviceHandle::Aura(aura) = dev_type.clone() { let path = dbus_path_for_tuf(); diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index dcd5b2038..f0db61a98 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -158,10 +158,7 @@ impl DeviceHandle { Ok(Self::Scsi(scsi)) } - pub async fn maybe_laptop_aura( - device: Option>>, - prod_id: &str, - ) -> Result { + pub async fn maybe_laptop_aura(prod_id: &str) -> Result { debug!("Testing for laptop aura"); let aura_type = AuraDeviceType::from(prod_id); if !matches!( @@ -203,14 +200,20 @@ impl DeviceHandle { 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) - } + (global, kbd, lb) }; + if dynamic_global.is_none() + && dynamic_kbd.is_none() + && dynamic_lightbar.is_none() + && backlight.is_none() + { + debug!("Neither Dynamic Lighting nor sysfs backlight detected"); + return Err(RogError::NotFound( + "No dynamic lighting or sysfs backlight found".to_string(), + )); + } + // 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; @@ -218,7 +221,6 @@ impl DeviceHandle { dynamic_global, dynamic_kbd, dynamic_lightbar, - hid: device, backlight, config: Arc::new(Mutex::new(config)), }; From c0006daef59d02268d48b5d295c0e87f903b1b74 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:12:20 +0200 Subject: [PATCH 07/12] refactor(asusd): clean up dead hidraw references in anime and device handles Remove unused `hid` (HidRaw) handle from `AniMe` struct and methods, as AniMe Matrix devices are driven exclusively via USB raw packets. - Remove `hid` field from `AniMe` and `AniMe::new`. - Remove dead `DeviceHandle::maybe_anime_hid` stub and its invocation in `init_hid_devices`. - Remove dead enum variants `Ally`, `OldAura`, and `TufLedClass` from `DeviceHandle`. --- asusd/src/aura_anime/mod.rs | 13 ++----------- asusd/src/aura_manager.rs | 23 +---------------------- asusd/src/aura_types.rs | 20 ++------------------ 3 files changed, 5 insertions(+), 51 deletions(-) diff --git a/asusd/src/aura_anime/mod.rs b/asusd/src/aura_anime/mod.rs index 5d622f3ab..84eb2b994 100644 --- a/asusd/src/aura_anime/mod.rs +++ b/asusd/src/aura_anime/mod.rs @@ -14,7 +14,6 @@ use rog_anime::usb::{ 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 +22,6 @@ use crate::error::RogError; #[derive(Debug, Clone)] pub struct AniMe { - hid: Option>>, usb: Option>>, config: Arc>, cache: AniMeConfigCached, @@ -34,13 +32,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,9 +71,7 @@ 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 { + if let Some(usb) = &self.usb { usb.lock().await.write_bytes(message)?; } Ok(()) diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index 744ec483e..46a9d98cd 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -221,27 +221,6 @@ impl DeviceManager { 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()), - }); - } - } - // ANIME MATRIX DEVICE - if let Ok(dev_type) = DeviceHandle::maybe_anime_hid(dev.clone(), usb_id_str).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, @@ -458,7 +437,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; } } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index f0db61a98..17d6eac7d 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -42,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, @@ -78,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(); @@ -103,17 +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(), - )) - } - pub async fn maybe_anime_usb() -> Result { debug!("Testing for USB AniMe"); let anime_type = get_anime_type(); @@ -128,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)), ); From a1a8b2a490a25aa972a0825054ee451cbd305876 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:20:33 +0200 Subject: [PATCH 08/12] feat(asusd): stream direct rgb packets through dynamic lighting direct_buffer Migrate per-key and zoned direct RGB addressing in write_effect_block from raw USB HID writes to the Dynamic Lighting direct_buffer sysfs interface exposed by the kernel driver (hid-asus). When Dynamic Lighting is available on the keyboard or global chassis node, convert the per-key chunked packets or 4-zone payloads into a flat RGB stream sized to match the hardware led_count, and write it directly to direct_buffer. The kernel driver handles packet chunking (opcode 0xbc), concurrency locking, and direct frame streaming without requiring userspace to open or manage /dev/hidraw. --- asusd/src/aura_laptop/mod.rs | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 8f7f8c8c7..203336ef4 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -294,8 +294,45 @@ impl Aura { 0, 0, r, g, b, 0, ])?; } + return Ok(()); } - Ok(()) + + let dynamic_led = self.dynamic_kbd.as_ref().or(self.dynamic_global.as_ref()); + if let Some(dynamic) = dynamic_led { + let dynamic = dynamic.lock().await; + let led_count = dynamic.get_led_count().unwrap_or(168) as usize; + let expected_len = led_count * 3; + let mut rgb_buf = Vec::with_capacity(expected_len); + + if effect.len() == 1 && led_count == 4 { + // Zoned keyboard (4 zones: left, left-mid, right-mid, right) + if let Some(row) = effect.first() + && let Some(payload) = row.get(9..21) + { + rgb_buf.extend_from_slice(payload); + } + } else { + // Per-key keyboard + for row in effect.iter() { + if row.len() >= 9 { + let num_leds = row.get(7).copied().unwrap_or(16) as usize; + let payload_len = num_leds * 3; + if let Some(payload) = row.get(9..9 + payload_len) { + rgb_buf.extend_from_slice(payload); + } + } + } + } + + if !rgb_buf.is_empty() { + rgb_buf.resize(expected_len, 0); + dynamic.write_direct(&rgb_buf)?; + config.per_key_mode_active = true; + } + return Ok(()); + } + + Err(RogError::NoAuraKeyboard) } pub async fn fix_ally_power(&mut self) -> Result<(), RogError> { From eb62893d4c682968ef2f1670f8843ffbec1f2eb0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:24:58 +0200 Subject: [PATCH 09/12] feat(asusd): discover tuf dynamic lighting on asus::kbd_backlight Update DynamicLed device discovery to support non-aura-prefixed LED class nodes that expose the Dynamic Lighting sysfs ABI (effect_index), such as asus::kbd_backlight registered by asus-wmi on TUF laptops. In DeviceHandle::maybe_laptop_aura, fall back to asus::kbd_backlight if aura:keyboard is absent. This allows TUF RGB laptop keyboards to be driven via the unified Dynamic Lighting sysfs path (effect, speed, palette) with graceful fallback to legacy platform sysfs when Dynamic Lighting is not supported by the kernel. --- asusd/src/aura_types.rs | 3 ++- rog-platform/src/dynamic_led.rs | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 17d6eac7d..3a1fc226b 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -173,8 +173,9 @@ impl DeviceHandle { }) .ok(); let kbd = DynamicLed::find("aura:keyboard") + .or_else(|_| DynamicLed::find("asus::kbd_backlight")) .map(|k| { - info!("Dynamic Lighting keyboard detected: aura:keyboard"); + info!("Dynamic Lighting keyboard detected: {}", k.name()); Arc::new(Mutex::new(k)) }) .ok(); diff --git a/rog-platform/src/dynamic_led.rs b/rog-platform/src/dynamic_led.rs index 9ca03568c..e3b48c312 100644 --- a/rog-platform/src/dynamic_led.rs +++ b/rog-platform/src/dynamic_led.rs @@ -50,10 +50,13 @@ impl DynamicLed { })? { let sysname = device.sysname().to_string_lossy(); if sysname == name { - info!("Found Dynamic Lighting LED device at {:?}", sysname); - return Ok(Self { - path: device.syspath().to_path_buf(), - }); + let syspath = device.syspath(); + if name.starts_with("aura:") || syspath.join("effect_index").exists() { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: syspath.to_path_buf(), + }); + } } } @@ -67,6 +70,14 @@ impl DynamicLed { 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() + } + /// Check if a dynamic LED is present on the system. pub fn is_available(name: &str) -> bool { Self::find(name).is_ok() From e90e9edf89ac177e191d662b92066e75f2e18aaf Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:32:50 +0200 Subject: [PATCH 10/12] feat(asusd): control slash lighting via sysfs led classdev and drop hidraw Add SlashLed wrapper in rog-platform targeting /sys/class/leds/asus::slash created by the kernel hid-asus driver. Update asusd's Slash implementation to drive brightness, animation mode, and interval via SlashLed sysfs attributes, retaining raw USB as a fallback for older kernels. Remove remaining HidRaw handles and device probing from aura_manager, completing the full removal of hidraw dependencies from the asusd daemon. --- asusd/src/aura_manager.rs | 81 +++++------------ asusd/src/aura_slash/mod.rs | 58 ++++++++----- asusd/src/aura_slash/trait_impls.rs | 129 ++++++++++++++++++---------- asusd/src/aura_types.rs | 41 ++++----- rog-platform/src/lib.rs | 2 + rog-platform/src/slash_led.rs | 65 ++++++++++++++ 6 files changed, 226 insertions(+), 150 deletions(-) create mode 100644 rog-platform/src/slash_led.rs diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index 46a9d98cd..adbdebe9d 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -4,7 +4,7 @@ // - 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 dmi_id::DMIID; @@ -12,7 +12,7 @@ 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 rog_slash::SlashType; use tokio::sync::Mutex; use udev::{Device, MonitorBuilder}; use zbus::Connection; @@ -98,7 +98,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 @@ -133,30 +132,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")? @@ -204,12 +182,16 @@ impl DeviceManager { return Ok(devices); } - // For other devices that still require a shared hid handle (e.g. Slash): - // 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_str).await + // Check for Slash device via sysfs SlashLed / USB + let slash_type = SlashType::from_dmi(); + if !matches!(slash_type, SlashType::Unsupported) + && slash_type + .prod_id_str() + .to_lowercase() + .trim_start_matches("0x") + == usb_id_str + { + if let Ok(dev_type) = DeviceHandle::maybe_slash().await && let DeviceHandle::Slash(slash) = dev_type.clone() { let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); @@ -225,22 +207,18 @@ impl DeviceManager { devices.push(AsusDevice { device: dev_type, dbus_path: path, - hid_key: Some(hid_key), + hid_key: None, }); } } - } 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 @@ -277,7 +255,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) @@ -417,13 +395,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 @@ -443,7 +418,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); @@ -534,19 +509,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()?; @@ -575,7 +547,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" { @@ -693,11 +664,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) = @@ -720,10 +686,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..a3b7bfc57 100644 --- a/asusd/src/aura_slash/mod.rs +++ b/asusd/src/aura_slash/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use config::SlashConfig; -use rog_platform::hid_raw::HidRaw; +use rog_platform::slash_led::SlashLed; use rog_platform::usb_raw::USBRaw; use rog_slash::usb::{slash_pkt_enable, slash_pkt_init, slash_pkt_options, slash_pkt_set_mode}; use tokio::sync::{Mutex, MutexGuard}; @@ -13,18 +13,22 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Slash { - hid: Option>>, + led: Option, usb: Option>>, config: Arc>, } impl Slash { pub fn new( - hid: Option>>, + led: Option, usb: Option>>, config: Arc>, ) -> Self { - Self { hid, usb, config } + Self { led, usb, config } + } + + pub fn led(&self) -> Option<&SlashLed> { + self.led.as_ref() } pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { @@ -32,9 +36,7 @@ impl Slash { } 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 { + if let Some(usb) = &self.usb { usb.lock().await.write_bytes(message)?; } Ok(()) @@ -43,26 +45,36 @@ impl Slash { /// 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?; + + if let Some(led) = &self.led { + let brightness = if config.enabled { config.brightness } else { 0 }; + led.set_brightness(brightness)?; + led.set_slash_interval(config.display_interval)?; + led.set_slash_mode(&config.display_mode.to_string())?; + return Ok(()); } - 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?; + if let Some(usb) = &self.usb { + for pkt in &slash_pkt_init(config.slash_type) { + usb.lock().await.write_bytes(pkt)?; + } + usb.lock() + .await + .write_bytes(&slash_pkt_enable(config.slash_type, config.enabled))?; - 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?; + // Apply config upon initialization + let option_packets = slash_pkt_options( + config.slash_type, + config.enabled, + config.brightness, + config.display_interval, + ); + usb.lock().await.write_bytes(&option_packets)?; + + let mode_packets = slash_pkt_set_mode(config.slash_type, config.display_mode); + usb.lock().await.write_bytes(&mode_packets[1])?; + } Ok(()) } diff --git a/asusd/src/aura_slash/trait_impls.rs b/asusd/src/aura_slash/trait_impls.rs index 70aa1ad36..ab86a418f 100644 --- a/asusd/src/aura_slash/trait_impls.rs +++ b/asusd/src/aura_slash/trait_impls.rs @@ -58,25 +58,33 @@ 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(); + + if let Some(led) = self.0.led() { + let b = if enabled { brightness } else { 0 }; + if let Err(err) = led.set_brightness(b) { + warn!("ctrl_slash::set_enabled via sysfs: {err}"); + } + } else { + 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(); + } config.enabled = enabled; config.brightness = brightness; @@ -95,18 +103,25 @@ 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 Some(led) = self.0.led() { + if let Err(err) = led.set_brightness(brightness) { + warn!("ctrl_slash::set_brightness via sysfs: {err}"); + } + } else { + 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(); + } config.enabled = enabled; config.brightness = brightness; @@ -123,15 +138,22 @@ 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 Some(led) = self.0.led() { + if let Err(err) = led.set_slash_interval(interval) { + warn!("ctrl_slash::set_interval via sysfs: {err}"); + } + } else { + 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(); + } config.display_interval = interval; config.write(); @@ -151,12 +173,18 @@ 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?; + if let Some(led) = self.0.led() { + led.set_slash_mode(&mode.to_string()).map_err(|err| { + zbus::fdo::Error::Failed(format!("ctrl_slash::set_mode sysfs: {err}")) + })?; + } else { + 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?; + } config.display_mode = mode; config.write(); @@ -280,6 +308,15 @@ impl Reloadable for SlashZbus { async fn reload(&mut self) -> Result<(), RogError> { debug!("reloading slash settings"); let config = self.0.lock_config().await; + + if let Some(led) = self.0.led() { + let brightness = if config.enabled { config.brightness } else { 0 }; + led.set_brightness(brightness)?; + led.set_slash_interval(config.display_interval)?; + led.set_slash_mode(&config.display_mode.to_string())?; + return Ok(()); + } + self.0 .write_bytes(&slash_pkt_options( config.slash_type, diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 3a1fc226b..035335cc5 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -7,7 +7,7 @@ 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::SlashLed; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; use rog_scsi::{ScsiType, open_device}; @@ -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,30 +47,26 @@ 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"); + /// Try Slash sysfs LED or USB. If one exists it is initialised and returned. + pub async fn maybe_slash() -> Result { + debug!("Testing for 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())); + if matches!(slash_type, SlashType::Unsupported) { + return Err(RogError::Slash(SlashError::NoDevice)); + } + + // Try sysfs SlashLed first (kernel driver hid-asus) + if let Ok(led) = SlashLed::new() { + info!("Found Slash sysfs LED at {:?}", led.path()); + let mut config = SlashConfig::new().load(); + config.slash_type = slash_type; + let slash = Slash::new(Some(led), None, Arc::new(Mutex::new(config))); + slash.do_initialization().await?; + return Ok(Self::Slash(slash)); } - info!("Found slash type {slash_type:?}: {prod_id}"); - let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new(Some(device), None, Arc::new(Mutex::new(config))); - slash.do_initialization().await?; - Ok(Self::Slash(slash)) + // Fallback to raw USB if kernel LED classdev is not present + Self::new_slash_usb().await } /// Try Slash USB. If one exists it is initialised and returned. diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index cecdd941f..6c2d19f69 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 + } +} From b725b16930d62811451abab244a4737ac90d80cb Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 09:32:39 +0200 Subject: [PATCH 11/12] feat(asusd): control scsi lighting via dynamic lighting sysfs Add generic ScsiLed abstraction in rog-platform leveraging the kernel's Dynamic Lighting interface (led_classdev_dynamic) exposed by the leds-asus-aura-scsi driver. Update ScsiAura in asusd to drive lighting exclusively through native kernel Dynamic Lighting (ScsiLed), and remove legacy userspace SG_IO ioctl mechanisms (sg.rs, scsi.rs, and libc dependency across the workspace). In aura_manager, discover native ScsiLed sysfs nodes directly, eliminating raw SCSI ioctls, retry loops, and the need for CAP_SYS_RAWIO. --- Cargo.lock | 1 - Cargo.toml | 1 - asusd/src/aura_manager.rs | 63 +--------- asusd/src/aura_scsi/mod.rs | 91 ++++++++++++-- asusd/src/aura_types.rs | 26 ++-- rog-platform/src/lib.rs | 2 + rog-platform/src/scsi_led.rs | 178 +++++++++++++++++++++++++++ rog-scsi/Cargo.toml | 1 - rog-scsi/src/builtin_modes.rs | 38 ------ rog-scsi/src/lib.rs | 7 -- rog-scsi/src/scsi.rs | 78 ------------ rog-scsi/src/sg.rs | 219 ---------------------------------- 12 files changed, 281 insertions(+), 424 deletions(-) create mode 100644 rog-platform/src/scsi_led.rs delete mode 100644 rog-scsi/src/scsi.rs delete mode 100644 rog-scsi/src/sg.rs diff --git a/Cargo.lock b/Cargo.lock index 50993ad42..109be3dd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4795,7 +4795,6 @@ dependencies = [ name = "rog_scsi" version = "6.4.0" dependencies = [ - "libc", "ron", "serde", "thiserror 2.0.20", diff --git a/Cargo.toml b/Cargo.toml index 1de9691a4..8f727797e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,6 @@ glam = { version = "^0.33.5", features = ["serde"] } image = "=0.25.10" inotify = "^0.11.5" ksni = { version = "^0.3.6", default-features = false, features = ["async-io"] } -libc = "^0.2.189" log = "^0.4.33" logind-zbus = { version = "^5.3.2" } mio = "^1.2.2" diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index adbdebe9d..115d50a95 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -261,31 +261,6 @@ impl DeviceManager { Ok(devices) } - /// Resolve the `/dev/sgN` (scsi_generic) node backing a block device. - /// - /// Walks up from the block device to its owning scsi_device and reads the - /// `scsi_generic/sgN` child. Works for whole-disk (`/dev/sda`) and - /// partition (`/dev/sda1`) nodes alike, since the scsi_device is a common - /// ancestor. Returns None if no sg node exists (e.g. the `sg` module is - /// not loaded). - fn sg_node_for_block(device: &Device) -> Option { - let mut current = device.parent(); - while let Some(d) = current { - if let Ok(entries) = std::fs::read_dir(d.syspath().join("scsi_generic")) { - for entry in entries.flatten() { - if let Some(name) = entry.file_name().to_str() { - let node = format!("/dev/{name}"); - if std::path::Path::new(&node).exists() { - return Some(node); - } - } - } - } - current = d.parent(); - } - None - } - async fn init_scsi( connection: &Connection, device: &Device, @@ -300,40 +275,9 @@ impl DeviceManager { .property_value("ID_MODEL_ID") .unwrap_or_default() .to_string_lossy(); - // SG_IO with vendor commands on the block node (/dev/sdX) - // requires CAP_SYS_RAWIO, which the hardened asusd unit drops - // (every ioctl EPERMs and is silently swallowed by write_effect). - // The scsi_generic /dev/sgN node gates access at open() via - // file permissions instead, so it works with no capabilities, - // the same path sg3_utils / OpenRGB use. - // - // On hotplug the sg node can appear just after the block node, - // so retry briefly before falling back to the block device - // (which would EPERM). At startup the node already exists, so - // the first attempt succeeds with no delay. - let mut sg_node = None; - for attempt in 0..8u8 { - if let Some(sg) = Self::sg_node_for_block(device) { - sg_node = Some(sg); - break; - } - if attempt < 7 { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } - let dev_str = match sg_node { - Some(sg) => Some(sg), - None => { - warn!( - "No /dev/sgN for SCSI device after retries; falling back to block \ - node {:?} (SG_IO will EPERM unless asusd has CAP_SYS_RAWIO)", - dev_node - ); - dev_node.as_os_str().to_str().map(|s| s.to_string()) - } - }; - if let Some(dev_str) = dev_str - && let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await + + let dev_str = dev_node.to_string_lossy(); + if let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await && let DeviceHandle::Scsi(scsi) = dev_type.clone() { let ctrl = ScsiZbus::new(scsi); @@ -353,6 +297,7 @@ impl DeviceManager { } } } + None } diff --git a/asusd/src/aura_scsi/mod.rs b/asusd/src/aura_scsi/mod.rs index 5e77ae47c..b62e00742 100644 --- a/asusd/src/aura_scsi/mod.rs +++ b/asusd/src/aura_scsi/mod.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use config::ScsiConfig; -use rog_scsi::{AuraEffect, Device, Task}; +use rog_platform::ScsiLed; +use rog_scsi::{AuraEffect, AuraMode, Direction}; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -11,13 +12,17 @@ pub mod trait_impls; #[derive(Clone)] pub struct ScsiAura { - device: Arc>, - config: Arc>, + pub led: ScsiLed, + pub config: Arc>, } impl ScsiAura { - pub fn new(device: Arc>, config: Arc>) -> Self { - Self { device, config } + pub fn new(led: ScsiLed, config: Arc>) -> Self { + Self { led, config } + } + + pub fn led(&self) -> &ScsiLed { + &self.led } pub async fn lock_config(&self) -> MutexGuard<'_, ScsiConfig> { @@ -25,13 +30,75 @@ impl ScsiAura { } pub async fn write_effect(&self, effect: &AuraEffect) -> Result<(), RogError> { - let mut tasks: Vec = effect.into(); - for task in &mut tasks { - // Surface the ioctl errno instead of dropping it — an EPERM/EIO - // here was previously invisible, so asusd reported success while - // no SCSI traffic ever reached the device. - if let Err(e) = self.device.lock().await.perform(task) { - log::warn!("SCSI perform failed: {e}"); + Self::write_kernel_effect(&self.led, effect) + } + + fn write_kernel_effect(led: &ScsiLed, effect: &AuraEffect) -> Result<(), RogError> { + match effect.mode { + AuraMode::Off => { + led.set_effect("off").map_err(RogError::Platform)?; + } + AuraMode::Static => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_effect("static").map_err(RogError::Platform)?; + } + AuraMode::Breathe => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("breathing").map_err(RogError::Platform)?; + } + AuraMode::Flashing => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("strobe").map_err(RogError::Platform)?; + } + AuraMode::RainbowCycle + | AuraMode::RainbowCycleBreathe + | AuraMode::RainbowPulseChase + | AuraMode::RandomFlicker + | AuraMode::DoubleFade => { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("spectrum_cycle") + .map_err(RogError::Platform)?; + } + AuraMode::RainbowWave + | AuraMode::Chase + | AuraMode::ChaseFade + | AuraMode::RainbowCycleChase + | AuraMode::RainbowCycleChaseFade + | AuraMode::RainbowCycleWave => { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + let dir = match effect.direction { + Direction::Forward => "right", + Direction::Reverse => "left", + }; + led.set_direction(dir).map_err(RogError::Platform)?; + led.set_effect("rainbow").map_err(RogError::Platform)?; } } Ok(()) diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 035335cc5..dbf6adf57 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -7,10 +7,11 @@ use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; use rog_aura::AuraDeviceType; use rog_platform::DynamicLed; +use rog_platform::ScsiLed; use rog_platform::SlashLed; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; -use rog_scsi::{ScsiType, open_device}; +use rog_scsi::ScsiType; use rog_slash::SlashType; use rog_slash::error::SlashError; use tokio::sync::Mutex; @@ -121,18 +122,27 @@ impl DeviceHandle { } pub async fn maybe_scsi(dev_node: &str, prod_id: &str) -> Result { - debug!("Testing for SCSI"); - let prod_id = ScsiType::from(prod_id); - if prod_id == ScsiType::Unsupported { - log::info!("Unknown or invalid SCSI: {prod_id:?}, skipping"); + let scsi_type = ScsiType::from(prod_id); + if scsi_type == ScsiType::Unsupported { + log::info!("Unknown or invalid SCSI: {scsi_type:?}, skipping"); return Err(RogError::NotFound("No SCSI device".to_string())); } - info!("Found SCSI device {prod_id:?} on {dev_node}"); + + let led = ScsiLed::find_for_dev(dev_node) + .or_else(|_| ScsiLed::new()) + .map_err(|e| { + log::warn!("No SCSI Dynamic Lighting device found for {dev_node}: {e}"); + RogError::NotFound("No SCSI Dynamic Lighting device found".to_string()) + })?; + + info!( + "Found SCSI Dynamic Lighting device {scsi_type:?} on {:?}", + led.path() + ); let mut config = ScsiConfig::new().load(); config.dev_type = AuraDeviceType::ScsiExtDisk; - let dev = Arc::new(Mutex::new(open_device(dev_node)?)); - let scsi = ScsiAura::new(dev, Arc::new(Mutex::new(config))); + let scsi = ScsiAura::new(led, Arc::new(Mutex::new(config))); scsi.do_initialization().await?; Ok(Self::Scsi(scsi)) } diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index 6c2d19f69..43efaeffb 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 scsi_led; pub mod slash_led; pub mod usb_raw; @@ -22,6 +23,7 @@ pub use dynamic_led::DynamicLed; use error::{PlatformError, Result}; use log::warn; use platform::PlatformProfile; +pub use scsi_led::ScsiLed; pub use slash_led::SlashLed; use udev::Device; diff --git a/rog-platform/src/scsi_led.rs b/rog-platform/src/scsi_led.rs new file mode 100644 index 000000000..b25503629 --- /dev/null +++ b/rog-platform/src/scsi_led.rs @@ -0,0 +1,178 @@ +use std::path::Path; + +use log::{info, warn}; + +use crate::dynamic_led::DynamicLed; +use crate::error::{PlatformError, Result}; + +/// Generic control interface for ASUS Aura SCSI-attached lighting devices +/// backed by the kernel `leds-asus-aura-scsi` Dynamic Lighting driver. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct ScsiLed { + dynamic: DynamicLed, +} + +impl ScsiLed { + /// Discover the first available ASUS SCSI dynamic lighting LED node. + pub fn new() -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("ScsiLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("ScsiLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("ScsiLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname.contains("asus-scsi") + || sysname.contains("asus-aura-scsi") + || sysname.contains("asus-arion") + { + info!( + "Found ASUS SCSI Dynamic Lighting LED device at {:?}", + sysname + ); + let dynamic = DynamicLed::find(&sysname)?; + return Ok(Self { dynamic }); + } + } + + Err(PlatformError::MissingFunction( + "ScsiLed::new(): no asus scsi dynamic LED device found".into(), + )) + } + + /// Find a `ScsiLed` instance corresponding to a given SCSI block device. + pub fn find_for_block(device: &udev::Device) -> Result { + // Walk up from block device to its scsi parent to get H:C:T:L (e.g. "2:0:0:0") + let mut current = device.parent(); + let mut hctl = None; + while let Some(d) = current { + if let Some(sub) = d.subsystem() + && sub == "scsi" + { + let s = d.sysname().to_string_lossy(); + if s.contains(':') { + hctl = Some(s.to_string()); + break; + } + } + current = d.parent(); + } + + if let Some(hctl_str) = hctl { + let sanitized = hctl_str.replace(':', "_"); + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("ScsiLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("ScsiLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for dev in enumerator.scan_devices().map_err(|err| { + warn!("ScsiLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = dev.sysname().to_string_lossy(); + let matches_name = sysname.contains("asus-scsi") + || sysname.contains("asus-aura-scsi") + || sysname.contains("asus-arion"); + let matches_hctl = sysname.contains(&sanitized) || sysname.contains(&hctl_str); + + if matches_name && matches_hctl { + info!( + "Found matched ASUS SCSI Dynamic Lighting LED at {:?}", + sysname + ); + let dynamic = DynamicLed::find(&sysname)?; + return Ok(Self { dynamic }); + } + } + } + + // Fallback: try finding any ASUS SCSI Dynamic Lighting LED + Self::new() + } + + /// Find a `ScsiLed` for a specific `/dev/sdX` or `/dev/sgN` path. + pub fn find_for_dev(dev_node: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|e| { + warn!("ScsiLed udev enumerator failed: {e}"); + PlatformError::Udev("enumerator failed".into(), e) + })?; + enumerator.match_subsystem("block").map_err(|e| { + warn!("ScsiLed match_subsystem failed: {e}"); + PlatformError::Udev("match block failed".into(), e) + })?; + + for dev in enumerator.scan_devices().map_err(|e| { + warn!("ScsiLed scan_devices failed: {e}"); + PlatformError::Udev("scan failed".into(), e) + })? { + if let Some(node) = dev.devnode() + && node.to_string_lossy() == dev_node + { + return Self::find_for_block(&dev); + } + } + + Self::new() + } + + /// Check if an ASUS SCSI Dynamic Lighting LED is available on the system. + pub fn is_available() -> bool { + Self::new().is_ok() + } + + /// Return reference to inner `DynamicLed`. + pub fn dynamic(&self) -> &DynamicLed { + &self.dynamic + } + + /// Return path to the sysfs node. + pub fn path(&self) -> &Path { + self.dynamic.path() + } + + /// Set animation effect string. + pub fn set_effect(&self, effect: &str) -> Result<()> { + self.dynamic.set_effect(effect) + } + + /// Set effect animation speed. + pub fn set_speed(&self, speed: u32) -> Result<()> { + self.dynamic.set_speed(speed) + } + + /// Set effect animation direction ("right" or "left"). + pub fn set_direction(&self, direction: &str) -> Result<()> { + self.dynamic.set_direction(direction) + } + + /// Set palette colors formatted as `(r, g, b)`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + self.dynamic.set_palette_colors(colors) + } + + /// Write raw RGB bytes to the direct buffer. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + self.dynamic.write_direct(data) + } + + /// Set brightness (0..=255). + pub fn set_brightness(&self, brightness: u8) -> Result<()> { + self.dynamic.set_brightness(brightness) + } + + /// Get brightness (0..=255). + pub fn get_brightness(&self) -> Result { + self.dynamic.get_brightness() + } +} diff --git a/rog-scsi/Cargo.toml b/rog-scsi/Cargo.toml index cafb9f1d2..6e8fba9ef 100644 --- a/rog-scsi/Cargo.toml +++ b/rog-scsi/Cargo.toml @@ -15,7 +15,6 @@ default = ["dbus", "ron"] dbus = ["zbus"] [dependencies] -libc.workspace = true serde.workspace = true zbus = { workspace = true, optional = true } diff --git a/rog-scsi/src/builtin_modes.rs b/rog-scsi/src/builtin_modes.rs index 0eb983cd6..4175e9655 100644 --- a/rog-scsi/src/builtin_modes.rs +++ b/rog-scsi/src/builtin_modes.rs @@ -6,8 +6,6 @@ use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Type, Value}; use crate::error::Error; -use crate::scsi::{apply_task, dir_task, mode_task, rgb_task, save_task, speed_task}; -use crate::sg::Task; #[cfg_attr(feature = "dbus", derive(Type, Value, OwnedValue))] #[derive(Debug, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)] @@ -358,39 +356,3 @@ impl Display for AuraEffect { writeln!(f, "}}") } } - -impl From<&AuraEffect> for Vec { - fn from(effect: &AuraEffect) -> Self { - let mut tasks = Vec::new(); - - tasks.append(&mut vec![ - mode_task(effect.mode as u8), - rgb_task(0, &effect.colour1.into()), - rgb_task(1, &effect.colour2.into()), - rgb_task(2, &effect.colour3.into()), - rgb_task(3, &effect.colour4.into()), - ]); - - if !matches!(effect.mode, AuraMode::Static | AuraMode::Off) { - tasks.push(speed_task(effect.speed as u8)); - } - if matches!( - effect.mode, - AuraMode::RainbowWave - | AuraMode::ChaseFade - | AuraMode::RainbowCycleChaseFade - | AuraMode::Chase - | AuraMode::RainbowCycleChase - | AuraMode::RainbowCycleWave - | AuraMode::RainbowPulseChase - ) { - tasks.push(dir_task(effect.direction as u8)); - } - - tasks.append(&mut vec![ - apply_task(), - save_task(), - ]); - tasks - } -} diff --git a/rog-scsi/src/lib.rs b/rog-scsi/src/lib.rs index 1fc382493..f300fb277 100644 --- a/rog-scsi/src/lib.rs +++ b/rog-scsi/src/lib.rs @@ -1,12 +1,9 @@ mod builtin_modes; mod error; -mod scsi; -pub mod sg; pub use builtin_modes::*; pub use error::*; use serde::{Deserialize, Serialize}; -pub use sg::{Device, Task}; pub const PROD_SCSI_ARION: &str = "1932"; @@ -43,7 +40,3 @@ impl From for &str { } } } - -pub fn open_device(path: &str) -> Result { - Device::open(path) -} diff --git a/rog-scsi/src/scsi.rs b/rog-scsi/src/scsi.rs deleted file mode 100644 index 0ae6af713..000000000 --- a/rog-scsi/src/scsi.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::sg::{Direction, Task}; - -static ENE_APPLY_VAL: u8 = 0x01; // Value for Apply Changes Register -static ENE_SAVE_VAL: u8 = 0xaa; - -static ENE_REG_MODE: u32 = 0x8021; // Mode Selection Register -static ENE_REG_SPEED: u32 = 0x8022; // Speed Control Register -static ENE_REG_DIRECTION: u32 = 0x8023; // Direction Control Register - -static ENE_REG_APPLY: u32 = 0x80a0; -static _ENE_REG_COLORS_DIRECT_V2: u32 = 0x8100; // to read the colurs -static ENE_REG_COLORS_EFFECT_V2: u32 = 0x8160; - -fn data(reg: u32, arg_count: u8) -> [u8; 16] { - let mut cdb = [0u8; 16]; - cdb[0] = 0xec; - cdb[1] = 0x41; - cdb[2] = 0x53; - cdb[3] = ((reg >> 8) & 0x00ff) as u8; - cdb[4] = (reg & 0x00ff) as u8; - cdb[5] = 0x00; - cdb[6] = 0x00; - cdb[7] = 0x00; - cdb[8] = 0x00; - cdb[9] = 0x00; - cdb[10] = 0x00; - cdb[11] = 0x00; - cdb[12] = 0x00; - cdb[13] = arg_count; // how many u8 in data packet - cdb[14] = 0x00; - cdb[15] = 0x00; - cdb -} - -pub(crate) fn rgb_task(led: u32, rgb: &[u8; 3]) -> Task { - let mut task = Task::new(); - task.set_cdb(data(led * 3 + ENE_REG_COLORS_EFFECT_V2, 3).as_slice()); - task.set_data(rgb, Direction::ToDevice); - task -} - -/// 0-13 -pub(crate) fn mode_task(mode: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_MODE, 1).as_slice()); - task.set_data(&[mode.min(13)], Direction::ToDevice); - task -} - -/// 0-4, fast to slow -pub(crate) fn speed_task(speed: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_SPEED, 1).as_slice()); - task.set_data(&[speed.min(4)], Direction::ToDevice); - task -} - -/// 0 = forward, 1 = backward -pub(crate) fn dir_task(mode: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_DIRECTION, 1).as_slice()); - task.set_data(&[mode.min(1)], Direction::ToDevice); - task -} - -pub(crate) fn apply_task() -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_APPLY, 1).as_slice()); - task.set_data(&[ENE_APPLY_VAL], Direction::ToDevice); - task -} - -pub(crate) fn save_task() -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_APPLY, 1).as_slice()); - task.set_data(&[ENE_SAVE_VAL], Direction::ToDevice); - task -} diff --git a/rog-scsi/src/sg.rs b/rog-scsi/src/sg.rs deleted file mode 100644 index c0359805e..000000000 --- a/rog-scsi/src/sg.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::ffi::c_void; -use std::fs::{File, OpenOptions}; -use std::io; -use std::os::unix::fs::OpenOptionsExt; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::path::Path; - -pub const SG_DXFER_NONE: i32 = -1; -pub const SG_DXFER_TO_DEV: i32 = -2; -pub const SG_DXFER_FROM_DEV: i32 = -3; -pub const SG_DXFER_TO_FROM_DEV: i32 = -4; - -pub const SG_INFO_OK_MASK: u32 = 0x1; -pub const SG_INFO_OK: u32 = 0x0; -pub const SG_IO: u64 = 0x2285; - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct SgIoHdr { - pub interface_id: std::os::raw::c_int, - pub dxfer_direction: std::os::raw::c_int, - pub cmd_len: u8, - pub mx_sb_len: u8, - pub iovec_count: u16, - pub dxfer_len: u32, - pub dxferp: *mut c_void, - pub cmdp: *mut u8, - pub sbp: *mut u8, - pub timeout: u32, - pub flags: u32, - pub pack_id: std::os::raw::c_int, - pub usr_ptr: *mut c_void, - pub status: u8, - pub masked_status: u8, - pub msg_status: u8, - pub sb_len_wr: u8, - pub host_status: u16, - pub driver_status: u16, - pub resid: i32, - pub duration: u32, - pub info: u32, -} - -impl Default for SgIoHdr { - fn default() -> Self { - Self { - interface_id: b'S' as std::os::raw::c_int, - dxfer_direction: SG_DXFER_NONE, - cmd_len: 0, - mx_sb_len: 0, - iovec_count: 0, - dxfer_len: 0, - dxferp: std::ptr::null_mut(), - cmdp: std::ptr::null_mut(), - sbp: std::ptr::null_mut(), - timeout: 0, - flags: 0, - pack_id: 0, - usr_ptr: std::ptr::null_mut(), - status: 0, - masked_status: 0, - msg_status: 0, - sb_len_wr: 0, - host_status: 0, - driver_status: 0, - resid: 0, - duration: 0, - info: 0, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Direction { - None, - ToDevice, - FromDevice, - ToFromDevice, -} - -impl Direction { - fn to_underlying(self) -> std::os::raw::c_int { - match self { - Direction::None => SG_DXFER_NONE, - Direction::ToDevice => SG_DXFER_TO_DEV, - Direction::FromDevice => SG_DXFER_FROM_DEV, - Direction::ToFromDevice => SG_DXFER_TO_FROM_DEV, - } - } -} - -#[derive(Clone, Debug)] -pub struct Task { - inner: SgIoHdr, - cmd: Vec, - data: Vec, - sense: Vec, -} - -impl Default for Task { - fn default() -> Self { - Self::new() - } -} - -// SAFETY: Task manages its internal buffers and SgIoHdr raw pointers. The raw pointers -// are updated prior to any ioctl execution to point directly to owned heap vectors, making -// Send and Sync safe across thread boundaries. -unsafe impl Send for Task {} -unsafe impl Sync for Task {} - -impl Task { - pub fn new() -> Self { - Task { - inner: SgIoHdr::default(), - cmd: Vec::new(), - data: Vec::new(), - sense: Vec::new(), - } - } - - /// Prepares raw internal pointers in SgIoHdr to match current buffer memory addresses. - fn sync_pointers(&mut self) { - if !self.cmd.is_empty() { - self.inner.cmdp = self.cmd.as_mut_ptr(); - self.inner.cmd_len = self.cmd.len() as u8; - } else { - self.inner.cmdp = std::ptr::null_mut(); - self.inner.cmd_len = 0; - } - - if !self.data.is_empty() { - self.inner.dxferp = self.data.as_mut_ptr() as *mut c_void; - self.inner.dxfer_len = self.data.len() as u32; - } else { - self.inner.dxferp = std::ptr::null_mut(); - self.inner.dxfer_len = 0; - } - - if !self.sense.is_empty() { - self.inner.sbp = self.sense.as_mut_ptr(); - self.inner.mx_sb_len = self.sense.len() as u8; - } else { - self.inner.sbp = std::ptr::null_mut(); - self.inner.mx_sb_len = 0; - } - } - - pub fn set_cdb(&mut self, buf: &[u8]) -> &mut Self { - self.cmd = buf.to_vec(); - self.sync_pointers(); - self - } - - pub fn set_data(&mut self, buf: &[u8], direction: Direction) -> &mut Self { - self.data = buf.to_vec(); - self.inner.dxfer_direction = direction.to_underlying(); - self.sync_pointers(); - self - } - - pub fn status(&self) -> u8 { - self.inner.status - } - - pub fn host_status(&self) -> u16 { - self.inner.host_status - } - - pub fn driver_status(&self) -> u16 { - self.inner.driver_status - } - - pub fn ok(&self) -> bool { - (self.inner.info & SG_INFO_OK_MASK) == SG_INFO_OK - } -} - -pub struct Device(File); - -impl Device { - pub fn open>(path: P) -> io::Result { - Ok(Device( - OpenOptions::new() - .read(true) - .write(true) - .custom_flags(libc::O_NONBLOCK) - .open(path)?, - )) - } - - /// Performs a synchronous SCSI IO operation via ioctl. On success the kernel - /// has written command status, sense data and any FromDevice payload back - /// into `task`, so results can be read via its accessors. - pub fn perform(&self, task: &mut Task) -> io::Result<()> { - task.sync_pointers(); - - #[cfg(target_env = "musl")] - let request = SG_IO as i32; - #[cfg(not(target_env = "musl"))] - let request: u64 = SG_IO; - - // SAFETY: The raw file descriptor is open and valid, and task has valid synced - // pointers into its own buffers, which stay alive for the duration of the - // synchronous ioctl. - let ret = unsafe { libc::ioctl(self.0.as_raw_fd(), request, &mut task.inner) }; - if ret == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } -} - -impl AsRawFd for Device { - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} From b1dedc2d672f593a1c338c1c62b9de2be4ff57fd Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 09:50:56 +0200 Subject: [PATCH 12/12] refactor(asusd): remove legacy raw usb fallback for slash lighting Drive Slash lighting exclusively through the sysfs SlashLed interface provided by the hid-asus kernel driver. Remove the USBRaw fallback handle and write_bytes wrapper from the Slash controller, and remove new_slash_usb from DeviceHandle. With hid-asus handling Slash Lighting in the kernel, userspace raw USB transfers are no longer required, simplifying the daemon architecture and eliminating unneeded device descriptor handles. --- asusd/src/aura_slash/mod.rs | 57 ++--------- asusd/src/aura_slash/trait_impls.rs | 153 ++++------------------------ asusd/src/aura_types.rs | 52 +++------- 3 files changed, 39 insertions(+), 223 deletions(-) diff --git a/asusd/src/aura_slash/mod.rs b/asusd/src/aura_slash/mod.rs index a3b7bfc57..b725eb73e 100644 --- a/asusd/src/aura_slash/mod.rs +++ b/asusd/src/aura_slash/mod.rs @@ -2,8 +2,6 @@ use std::sync::Arc; use config::SlashConfig; use rog_platform::slash_led::SlashLed; -use rog_platform::usb_raw::USBRaw; -use rog_slash::usb::{slash_pkt_enable, slash_pkt_init, slash_pkt_options, slash_pkt_set_mode}; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -13,69 +11,32 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Slash { - led: Option, - usb: Option>>, + led: SlashLed, config: Arc>, } impl Slash { - pub fn new( - led: Option, - usb: Option>>, - config: Arc>, - ) -> Self { - Self { led, usb, config } + pub fn new(led: SlashLed, config: Arc>) -> Self { + Self { led, config } } - pub fn led(&self) -> Option<&SlashLed> { - self.led.as_ref() + pub fn led(&self) -> &SlashLed { + &self.led } pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { self.config.lock().await } - pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; - } - Ok(()) - } - /// Initialise the device if required. Locks the internal config so be wary /// of deadlocks. pub async fn do_initialization(&self) -> Result<(), RogError> { let config = self.config.lock().await; - if let Some(led) = &self.led { - let brightness = if config.enabled { config.brightness } else { 0 }; - led.set_brightness(brightness)?; - led.set_slash_interval(config.display_interval)?; - led.set_slash_mode(&config.display_mode.to_string())?; - return Ok(()); - } - - if let Some(usb) = &self.usb { - for pkt in &slash_pkt_init(config.slash_type) { - usb.lock().await.write_bytes(pkt)?; - } - usb.lock() - .await - .write_bytes(&slash_pkt_enable(config.slash_type, config.enabled))?; - - // Apply config upon initialization - let option_packets = slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - ); - usb.lock().await.write_bytes(&option_packets)?; - - let mode_packets = slash_pkt_set_mode(config.slash_type, config.display_mode); - usb.lock().await.write_bytes(&mode_packets[1])?; - } - + 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 ab86a418f..f37e62585 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}; @@ -26,7 +21,6 @@ impl SlashZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -59,31 +53,9 @@ impl SlashZbus { config.brightness }; - if let Some(led) = self.0.led() { - let b = if enabled { brightness } else { 0 }; - if let Err(err) = led.set_brightness(b) { - warn!("ctrl_slash::set_enabled via sysfs: {err}"); - } - } else { - 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; @@ -104,23 +76,8 @@ impl SlashZbus { let mut config = self.0.lock_config().await; let enabled = brightness > 0; - if let Some(led) = self.0.led() { - if let Err(err) = led.set_brightness(brightness) { - warn!("ctrl_slash::set_brightness via sysfs: {err}"); - } - } else { - 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; @@ -139,20 +96,8 @@ impl SlashZbus { async fn set_interval(&self, interval: u8) { let mut config = self.0.lock_config().await; - if let Some(led) = self.0.led() { - if let Err(err) = led.set_slash_interval(interval) { - warn!("ctrl_slash::set_interval via sysfs: {err}"); - } - } else { - 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; @@ -165,7 +110,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| { @@ -173,18 +118,12 @@ impl SlashZbus { })?; let mut config = self.0.lock_config().await; - if let Some(led) = self.0.led() { - led.set_slash_mode(&mode.to_string()).map_err(|err| { + self.0 + .led() + .set_slash_mode(&mode.to_string()) + .map_err(|err| { zbus::fdo::Error::Failed(format!("ctrl_slash::set_mode sysfs: {err}")) })?; - } else { - 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?; - } config.display_mode = mode; config.write(); @@ -192,7 +131,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) @@ -207,9 +145,6 @@ 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(()) @@ -224,9 +159,6 @@ 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(()) @@ -241,9 +173,6 @@ 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(()) @@ -258,9 +187,6 @@ 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(()) @@ -275,9 +201,6 @@ 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(()) @@ -292,12 +215,6 @@ 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(()) @@ -309,48 +226,12 @@ impl Reloadable for SlashZbus { debug!("reloading slash settings"); let config = self.0.lock_config().await; - if let Some(led) = self.0.led() { - let brightness = if config.enabled { config.brightness } else { 0 }; - led.set_brightness(brightness)?; - led.set_slash_interval(config.display_interval)?; - led.set_slash_mode(&config.display_mode.to_string())?; - return Ok(()); - } - + 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 - .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" - ); + .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 dbf6adf57..cc1bd48e9 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -1,7 +1,7 @@ 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; @@ -48,7 +48,7 @@ pub enum DeviceHandle { } impl DeviceHandle { - /// Try Slash sysfs LED or USB. If one exists it is initialised and returned. + /// Try Slash sysfs LED. If one exists it is initialised and returned. pub async fn maybe_slash() -> Result { debug!("Testing for Slash"); let slash_type = SlashType::from_dmi(); @@ -56,43 +56,17 @@ impl DeviceHandle { return Err(RogError::Slash(SlashError::NoDevice)); } - // Try sysfs SlashLed first (kernel driver hid-asus) - if let Ok(led) = SlashLed::new() { - info!("Found Slash sysfs LED at {:?}", led.path()); - let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new(Some(led), None, Arc::new(Mutex::new(config))); - slash.do_initialization().await?; - return Ok(Self::Slash(slash)); - } - - // Fallback to raw USB if kernel LED classdev is not present - Self::new_slash_usb().await - } - - /// 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())) - } + 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(led, Arc::new(Mutex::new(config))); + slash.do_initialization().await?; + Ok(Self::Slash(slash)) } pub async fn maybe_anime_usb() -> Result {