From c13cef9a1f7bc1e5b9a720491a59a40a3f3dfceb Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 11:53:39 +0100 Subject: [PATCH 1/6] feat(rog-platform): add Dynamic Lighting sysfs helpers Add DynamicLed for the led-class-dynamic ABI (including direct_sample_size with legacy heuristics fallback), dual-path SlashLed (DL effect/speed/power_states or legacy slash_mode), and XboxLogoLed with optional DL effect/speed. Signed-off-by: Marco Scardovi --- rog-platform/src/dynamic_led.rs | 359 ++++++++++++++++++++++++++++++ rog-platform/src/hid_raw.rs | 81 ++++++- rog-platform/src/lib.rs | 20 +- rog-platform/src/slash_led.rs | 157 +++++++++++++ rog-platform/src/xbox_logo_led.rs | 95 ++++++++ 5 files changed, 699 insertions(+), 13 deletions(-) create mode 100644 rog-platform/src/dynamic_led.rs create mode 100644 rog-platform/src/slash_led.rs create mode 100644 rog-platform/src/xbox_logo_led.rs diff --git a/rog-platform/src/dynamic_led.rs b/rog-platform/src/dynamic_led.rs new file mode 100644 index 000000000..1baec4c6e --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,359 @@ +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +/// Default RGB packed layout: three bytes per LED. +pub const DIRECT_SAMPLE_RGB: usize = 3; +/// Mono intensity layout used by AniMe Matrix Dynamic Lighting. +pub const DIRECT_SAMPLE_MONO: usize = 1; + +/// 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!("power_states", path); + + attr_string!("power_states_index", path); + + attr_string!("enabled", path); + + attr_string!("enabled_index", path); + + attr_num!("speed", path, u32); + + attr_num!("max_palette_entries", path, u32); + + attr_num!("led_count", path, u32); + + attr_num!("direct_sample_size", path, u32); + + attr_num!("brightness", path, u8); + + attr_num!("max_brightness", path, u8); + + /// Create a new `DynamicLed` by matching the exact sysfs name (e.g. + /// `"asus::keyboard"`). + pub fn new(name: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("DynamicLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("DynamicLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("DynamicLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname == name { + let syspath = device.syspath(); + if Self::is_dynamic_node(syspath) { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: syspath.to_path_buf(), + }); + } + } + } + + Err(PlatformError::MissingFunction(format!( + "DynamicLed::new(): no dynamic LED named '{name}' found" + ))) + } + + /// Helper to find a dynamic LED by name. + pub fn find(name: &str) -> Result { + Self::new(name) + } + + /// Wrap an already-resolved sysfs path if it validates as Dynamic Lighting. + pub fn from_sysfs_path(path: PathBuf) -> Result { + if Self::is_dynamic_node(&path) { + Ok(Self { path }) + } else { + Err(PlatformError::MissingFunction(format!( + "DynamicLed::from_sysfs_path(): {:?} is not a Dynamic Lighting node", + path + ))) + } + } + + /// Return the LED name (e.g. "asus::keyboard"). + pub fn name(&self) -> &str { + self.path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + } + + /// Sysfs directory for this LED. + pub fn path(&self) -> &Path { + &self.path + } + + pub fn is_dynamic_node(path: &Path) -> bool { + [ + "effect", "effect_index", "zone_type", "led_count", + ] + .iter() + .all(|attr| path.join(attr).exists()) + } + + /// Bytes per LED in `direct_buffer`. + /// + /// Prefer the kernel `direct_sample_size` sysfs attribute when present. + /// Older kernels omit it: fall back to mono for AniMe / `zone_type=matrix`, + /// otherwise packed RGB. + pub fn direct_sample_size(&self) -> usize { + if self.has_direct_sample_size() + && let Ok(size) = self.get_direct_sample_size() + && size > 0 + { + return size as usize; + } + if self.name().contains("anime") { + return DIRECT_SAMPLE_MONO; + } + if self.has_zone_type() + && self + .get_zone_type() + .ok() + .is_some_and(|z| z.trim() == "matrix") + { + return DIRECT_SAMPLE_MONO; + } + DIRECT_SAMPLE_RGB + } + + /// Read and parse space-separated list of supported effects from + /// `effect_index`. + pub fn get_supported_effects_list(&self) -> Result> { + let raw = self.get_effect_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Check effect support while preserving sysfs read errors. + pub fn supports_effect(&self, effect: &str) -> Result { + Ok(self + .get_supported_effects_list()? + .iter() + .any(|candidate| candidate == effect)) + } + + /// Read and parse space-separated list of supported directions from + /// `direction_index`. + pub fn get_supported_directions_list(&self) -> Result> { + let raw = self.get_direction_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Lighting engine on/off from the optional `enabled` attribute. + pub fn is_lighting_enabled(&self) -> Result { + match self.get_enabled()?.trim() { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(PlatformError::InvalidValue), + } + } + + /// Set lighting engine on/off. Kernel values are `true` / `false`. + pub fn set_lighting_enabled(&self, enabled: bool) -> Result<()> { + if !self.has_enabled() { + return Err(PlatformError::AttrNotFound("enabled".into())); + } + self.set_enabled(if enabled { "true" } else { "false" }) + } + + /// Set speed after validating the optional advertised inclusive range. + pub fn set_supported_speed(&self, speed: u32) -> Result<()> { + if !self.has_speed() { + return Err(PlatformError::AttrNotFound("speed".into())); + } + if self.has_speed_range() { + let (min, max) = parse_speed_range(&self.get_speed_range()?)?; + if !(min..=max).contains(&speed) { + return Err(PlatformError::InvalidValue); + } + } + self.set_speed(speed) + } + + /// Set direction after validating the optional advertised values. + pub fn set_supported_direction(&self, direction: &str) -> Result<()> { + if !self.has_direction() { + return Err(PlatformError::AttrNotFound("direction".into())); + } + if self.has_direction_index() + && !self + .get_supported_directions_list()? + .iter() + .any(|candidate| candidate == direction) + { + return Err(PlatformError::InvalidValue); + } + self.set_direction(direction) + } + + /// Write `power_states`, including an intentional empty list. + /// + /// `write_attr_string` trims, so a clearing write must bypass it: the kernel + /// accepts a lone newline as "no states". + pub fn set_power_states_list(&self, states: &[&str]) -> Result<()> { + if !self.has_power_states() { + return Err(PlatformError::AttrNotFound("power_states".into())); + } + let path = self.path.join("power_states"); + let payload = if states.is_empty() { + "\n".to_string() + } else { + let mut joined = states.join(" "); + joined.push('\n'); + joined + }; + let mut file = OpenOptions::new() + .write(true) + .open(&path) + .map_err(|e| PlatformError::IoPath(path.to_string_lossy().into_owned(), e))?; + file.write_all(payload.as_bytes()) + .map_err(|e| PlatformError::IoPath(path.to_string_lossy().into_owned(), e)) + } + + /// Write one complete frame to the `direct_buffer` binary attribute. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + let led_count = self.get_led_count()? as usize; + validate_direct_len(led_count, self.direct_sample_size(), data.len())?; + + let direct_path = self.path.join("direct_buffer"); + let mut file = OpenOptions::new() + .write(true) + .open(&direct_path) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e))?; + file.write_all(data) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e)) + } + + /// Write palette colors as formatted `"#RRGGBB #RRGGBB ..."` string to + /// `effects_palette`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + if !self.has_effects_palette() { + return Err(PlatformError::AttrNotFound("effects_palette".into())); + } + if self.has_max_palette_entries() && colors.len() > self.get_max_palette_entries()? as usize + { + return Err(PlatformError::InvalidValue); + } + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + self.set_effects_palette(&palette_str) + } +} + +fn validate_direct_len(led_count: usize, sample_size: usize, data_len: usize) -> Result<()> { + let expected = led_count + .checked_mul(sample_size) + .ok_or(PlatformError::InvalidValue)?; + if data_len == expected { + Ok(()) + } else { + Err(PlatformError::InvalidValue) + } +} + +pub fn parse_speed_range(raw: &str) -> Result<(u32, u32)> { + let values: Vec<_> = raw + .split(|c: char| !c.is_ascii_digit()) + .filter(|value| !value.is_empty()) + .map(str::parse::) + .collect::>() + .map_err(|_| PlatformError::ParseNum)?; + match values.as_slice() { + [min, max] if min <= max => Ok((*min, *max)), + _ => Err(PlatformError::InvalidValue), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + #[test] + fn test_palette_colors_formatting() { + let colors = [ + (255, 0, 128), + (0, 255, 64), + ]; + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + assert_eq!(palette_str, "#ff0080 #00ff40"); + } + + #[test] + fn validates_exact_direct_frame_size() { + assert!(super::validate_direct_len(4, 3, 12).is_ok()); + assert!(super::validate_direct_len(4, 3, 11).is_err()); + assert!(super::validate_direct_len(810, 1, 810).is_ok()); + assert!(super::validate_direct_len(810, 1, 2430).is_err()); + assert!(super::validate_direct_len(usize::MAX, 3, 0).is_err()); + } + + #[test] + fn parses_bounded_speed_range() { + assert_eq!(super::parse_speed_range("0-2\n").unwrap(), (0, 2)); + assert_eq!(super::parse_speed_range("0 2\n").unwrap(), (0, 2)); + assert_eq!(super::parse_speed_range("[1-4]").unwrap(), (1, 4)); + assert!(super::parse_speed_range("fast slow").is_err()); + assert!(super::parse_speed_range("4 1").is_err()); + } + + #[test] + fn validates_mandatory_generic_attributes() { + let path = + std::env::temp_dir().join(format!("asusctl-dynamic-led-test-{}", std::process::id())); + let _ = fs::remove_dir_all(&path); + fs::create_dir(&path).expect("temporary test directory must be creatable"); + for attr in [ + "effect", "effect_index", "zone_type", "led_count", + ] { + fs::write(path.join(attr), b"").expect("temporary attribute must be writable"); + } + assert!(super::DynamicLed::is_dynamic_node(&path)); + fs::remove_file(path.join("zone_type")).expect("test attribute must be removable"); + assert!(!super::DynamicLed::is_dynamic_node(&path)); + fs::remove_dir_all(path).expect("temporary test directory must be removable"); + } +} diff --git a/rog-platform/src/hid_raw.rs b/rog-platform/src/hid_raw.rs index ec9c17373..bf5e679d6 100644 --- a/rog-platform/src/hid_raw.rs +++ b/rog-platform/src/hid_raw.rs @@ -21,6 +21,12 @@ pub struct HidRaw { } impl HidRaw { + /// Check whether a hidraw endpoint descriptor declares an output report ID. + pub fn supports_output_report(endpoint: &Device, report_id: u8) -> bool { + std::fs::read(endpoint.syspath().join("device/report_descriptor")) + .is_ok_and(|descriptor| descriptor_has_output_report(&descriptor, report_id)) + } + pub fn new(id_product: &str) -> Result { let mut enumerator = udev::Enumerator::new().map_err(|err| { warn!("{}", err); @@ -87,7 +93,7 @@ impl HidRaw { file: RefCell::new(OpenOptions::new().write(true).open(dev_node)?), devfs_path: dev_node.to_owned(), prod_id: id_product.to_string_lossy().into(), - _device_bcd: endpoint + _device_bcd: parent .attribute_value("bcdDevice") .unwrap_or_default() .to_string_lossy() @@ -106,12 +112,73 @@ impl HidRaw { /// Write an array of raw bytes to the device using the hidraw interface pub fn write_bytes(&self, message: &[u8]) -> Result<()> { - if let Ok(mut file) = self.file.try_borrow_mut() { - // TODO: re-get the file if error? - file.write_all(message).map_err(|e| { - PlatformError::IoPath(self.devfs_path.to_string_lossy().to_string(), e) - })?; + if message.is_empty() { + return Err(PlatformError::InvalidValue); + } + let mut file = self + .file + .try_borrow_mut() + .map_err(|_| PlatformError::InvalidValue)?; + file.write_all(message) + .map_err(|e| PlatformError::IoPath(self.devfs_path.to_string_lossy().to_string(), e)) + } +} + +fn descriptor_has_output_report(descriptor: &[u8], wanted_id: u8) -> bool { + let mut offset = 0; + let mut report_id = 0; + while let Some(prefix) = descriptor.get(offset).copied() { + if prefix == 0xfe { + let Some(size) = descriptor.get(offset + 1).copied() else { + return false; + }; + offset = match offset.checked_add(3 + usize::from(size)) { + Some(next) => next, + None => return false, + }; + continue; } - Ok(()) + let size = match prefix & 0x03 { + 3 => 4, + value => usize::from(value), + }; + let end = match offset.checked_add(1 + size) { + Some(end) => end, + None => return false, + }; + let Some(data) = descriptor.get(offset + 1..end) else { + return false; + }; + let item_type = (prefix >> 2) & 0x03; + let tag = prefix >> 4; + if item_type == 1 && tag == 8 && size == 1 { + report_id = data[0]; + } else if item_type == 0 && tag == 9 && report_id == wanted_id { + return true; + } + offset = end; + } + false +} + +#[cfg(test)] +mod tests { + use super::descriptor_has_output_report; + + #[test] + fn parses_output_report_id_safely() { + assert!(descriptor_has_output_report( + &[ + 0x85, 0x5d, 0x09, 0x01, 0x91, 0x02 + ], + 0x5d + )); + assert!(!descriptor_has_output_report( + &[ + 0x85, 0x5d, 0x09, 0x01, 0x81, 0x02 + ], + 0x5d + )); + assert!(!descriptor_has_output_report(&[0x85], 0x5d)); } } diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index dec0bb861..96cf60e1f 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; @@ -12,14 +13,19 @@ pub mod keyboard_led; pub(crate) mod macros; pub mod platform; pub mod power; +pub mod slash_led; pub mod usb_raw; +pub mod xbox_logo_led; use std::path::Path; +pub use dynamic_led::DynamicLed; use error::{PlatformError, Result}; use log::warn; use platform::PlatformProfile; +pub use slash_led::SlashLed; use udev::Device; +pub use xbox_logo_led::XboxLogoLed; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -74,13 +80,15 @@ pub fn write_attr_num(device: &mut Device, attr_name: &str, value: T) -> Resu where T: std::fmt::Display, { - if device + device .set_attribute_value(attr_name, format!("{value}")) - .is_err() - { - return Err(PlatformError::AttrNotFound(attr_name.to_owned())); - } - Ok(()) + .map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + PlatformError::AttrNotFound(attr_name.to_owned()) + } else { + PlatformError::IoPath(attr_name.to_owned(), e) + } + }) } pub fn read_attr_u8_array(device: &Device, attr_name: &str) -> Result> { diff --git a/rog-platform/src/slash_led.rs b/rog-platform/src/slash_led.rs new file mode 100644 index 000000000..117244fbd --- /dev/null +++ b/rog-platform/src/slash_led.rs @@ -0,0 +1,157 @@ +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::dynamic_led::DynamicLed; +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +/// Slash chassis LED (`asus::slash`). +/// +/// Supports both the legacy custom attributes (`slash_mode` / +/// `slash_interval`) and a full Dynamic Lighting node (`effect` / `speed` / +/// optional `power_states`). Detection prefers Dynamic Lighting when the +/// generic attrs validate. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct SlashLed { + path: PathBuf, + dynamic: Option, +} + +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() { + return Self::from_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(); + // Exact chassis node only — do not bind peripheral LEDs whose + // name merely contains "slash". + if sys == "asus::slash" { + return Self::from_path(device.syspath().to_owned()); + } + } + + Err(PlatformError::MissingFunction( + "SlashLed:new(), asus::slash not found".into(), + )) + } + + fn from_path(path: PathBuf) -> Result { + let dynamic = DynamicLed::from_sysfs_path(path.clone()).ok(); + if dynamic.is_some() { + info!("Found Slash Dynamic Lighting LED at {:?}", path); + } else { + info!("Found Slash LED (legacy attrs) at {:?}", path); + } + Ok(Self { path, dynamic }) + } + + pub fn is_available() -> bool { + Self::new().is_ok() + } + + pub fn path(&self) -> &Path { + &self.path + } + + /// True when the node speaks the generic Dynamic Lighting ABI. + pub fn is_dynamic(&self) -> bool { + self.dynamic.is_some() + } + + pub fn has_power_states(&self) -> bool { + self.dynamic + .as_ref() + .is_some_and(|led| led.has_power_states()) + } + + pub fn get_power_states(&self) -> Result { + let led = self + .dynamic + .as_ref() + .ok_or_else(|| PlatformError::AttrNotFound("power_states".into()))?; + led.get_power_states() + } + + pub fn set_power_states_list(&self, states: &[&str]) -> Result<()> { + let led = self + .dynamic + .as_ref() + .ok_or_else(|| PlatformError::AttrNotFound("power_states".into()))?; + led.set_power_states_list(states) + } + + /// Select Slash animation mode (`effect` under DL, else `slash_mode`). + pub fn set_mode(&self, mode: &str) -> Result<()> { + if let Some(led) = &self.dynamic { + return led.set_effect(mode); + } + self.set_slash_mode(mode) + } + + /// Read current mode name. + pub fn get_mode(&self) -> Result { + if let Some(led) = &self.dynamic { + return led.get_effect(); + } + self.get_slash_mode() + } + + /// Space-separated mode index. + pub fn get_mode_index(&self) -> Result { + if let Some(led) = &self.dynamic { + return led.get_effect_index(); + } + self.get_slash_mode_index() + } + + /// Animation interval / speed. + pub fn set_interval(&self, interval: u8) -> Result<()> { + if let Some(led) = &self.dynamic { + if led.has_speed() { + return led.set_supported_speed(u32::from(interval)); + } + if self.has_slash_interval() { + return self.set_slash_interval(interval); + } + return Err(PlatformError::AttrNotFound("speed".into())); + } + self.set_slash_interval(interval) + } + + pub fn get_interval(&self) -> Result { + if let Some(led) = &self.dynamic { + if led.has_speed() { + return Ok(led.get_speed()? as u8); + } + if self.has_slash_interval() { + return self.get_slash_interval(); + } + return Err(PlatformError::AttrNotFound("speed".into())); + } + self.get_slash_interval() + } +} diff --git a/rog-platform/src/xbox_logo_led.rs b/rog-platform/src/xbox_logo_led.rs new file mode 100644 index 000000000..51ed1dcca --- /dev/null +++ b/rog-platform/src/xbox_logo_led.rs @@ -0,0 +1,95 @@ +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::dynamic_led::DynamicLed; +use crate::error::{PlatformError, Result}; +use crate::{attr_num, to_device}; + +/// Xbox Guide LED on Xbox Ally X (`asus::xbox_logo`, brightness 0–3). +/// +/// Prefers a Dynamic Lighting node when `effect`/`effect_index` validate; +/// otherwise uses brightness-only class LED attributes. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct XboxLogoLed { + path: PathBuf, + dynamic: Option, +} + +impl XboxLogoLed { + attr_num!("brightness", path, u8); + attr_num!("max_brightness", path, u8); + + pub fn new() -> Result { + let std_path = Path::new("/sys/class/leds/asus::xbox_logo"); + if std_path.exists() { + return Self::from_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 == "asus::xbox_logo" || sys.contains("xbox_logo") { + return Self::from_path(device.syspath().to_owned()); + } + } + + Err(PlatformError::MissingFunction( + "XboxLogoLed:new(), asus::xbox_logo not found".into(), + )) + } + + fn from_path(path: PathBuf) -> Result { + let dynamic = DynamicLed::from_sysfs_path(path.clone()).ok(); + if dynamic.is_some() { + info!("Found Xbox Guide Dynamic Lighting LED at {:?}", path); + } else { + info!("Found Xbox Guide LED at {:?}", path); + } + Ok(Self { path, dynamic }) + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn is_dynamic(&self) -> bool { + self.dynamic.is_some() + } + + pub fn set_effect(&self, effect: &str) -> Result<()> { + let led = self + .dynamic + .as_ref() + .ok_or_else(|| PlatformError::AttrNotFound("effect".into()))?; + led.set_effect(effect) + } + + pub fn get_effect(&self) -> Result { + let led = self + .dynamic + .as_ref() + .ok_or_else(|| PlatformError::AttrNotFound("effect".into()))?; + led.get_effect() + } + + pub fn set_speed(&self, speed: u32) -> Result<()> { + let led = self + .dynamic + .as_ref() + .ok_or_else(|| PlatformError::AttrNotFound("speed".into()))?; + led.set_supported_speed(speed) + } +} From b49a9fd3caab9b0ae16a9a990749055892aba226 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 11:53:41 +0100 Subject: [PATCH 2/6] feat(rog-aura): map Aura modes to Dynamic Lighting effect names Translate builtin Aura modes, speeds, and directions to the kernel Dynamic Lighting vocabulary, including Ally reactive. Signed-off-by: Marco Scardovi --- rog-aura/src/builtin_modes.rs | 241 +++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 2 deletions(-) diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..e0c01c8e4 100644 --- a/rog-aura/src/builtin_modes.rs +++ b/rog-aura/src/builtin_modes.rs @@ -40,6 +40,46 @@ impl LedBrightness { Self::High => Self::Med, } } + + /// Map the 4-step UI level onto a sysfs brightness value. + /// + /// When `max_brightness <= 3` the legacy 0..=3 mapping is kept. Otherwise + /// levels are scaled across `0..=max_brightness` (Off=0, High=max). + pub const fn to_scaled(self, max_brightness: u8) -> u8 { + if max_brightness <= 3 { + return self as u8; + } + match self { + Self::Off => 0, + Self::Low => max_brightness / 3, + Self::Med => ((2u16 * max_brightness as u16) / 3) as u8, + Self::High => max_brightness, + } + } + + /// Inverse of [`Self::to_scaled`]. + pub const fn from_scaled(value: u8, max_brightness: u8) -> Self { + if max_brightness <= 3 { + return match value { + 0 => Self::Off, + 1 => Self::Low, + 3 => Self::High, + _ => Self::Med, + }; + } + if value == 0 { + return Self::Off; + } + let low = max_brightness / 3; + let med = ((2u16 * max_brightness as u16) / 3) as u8; + if value <= low { + Self::Low + } else if value <= med { + Self::Med + } else { + Self::High + } + } } impl From for LedBrightness { @@ -199,6 +239,33 @@ impl From for u8 { } } } + +impl Speed { + /// Default ASUS Dynamic Lighting mapping for a `0..=2` `speed_range`. + pub const fn to_dynamic_speed(&self) -> u32 { + match self { + Self::Low => 0, + Self::Med => 1, + Self::High => 2, + } + } + + /// Map Low/Med/High onto an inclusive kernel `speed_range`. + /// + /// Keeps `0..=2` boards on 0/1/2 and lifts boards that advertise `1..=3` + /// (or any other span) so Low is never rejected as out of range. + pub const fn to_dynamic_speed_in_range(self, min: u32, max: u32) -> u32 { + if max <= min { + return min; + } + let step = match self { + Self::Low => 0u32, + Self::Med => 1, + Self::High => 2, + }; + min + (step * (max - min)).div_ceil(2) + } +} /// Used for Rainbow mode. /// /// Enum corresponds to the required integer value @@ -248,6 +315,17 @@ impl From for i32 { } } +impl Direction { + pub const fn to_dynamic_direction_str(&self) -> &'static str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Up => "up", + Self::Down => "down", + } + } +} + /// Enum of modes that convert to the actual number required by a USB HID packet #[cfg_attr( feature = "dbus", @@ -271,6 +349,10 @@ pub enum AuraModeNum { Pulse = 10, Comet = 11, Flash = 12, + /// Kernel `direct` effect: per-key frames go through `direct_buffer`. + Direct = 13, + /// Ally / Xbox Ally MCU reactive (`0x0e`). Advertised only on Xbox Ally X. + Reactive = 14, } impl Display for AuraModeNum { @@ -300,6 +382,8 @@ impl From<&AuraModeNum> for &str { AuraModeNum::Pulse => "Pulse", AuraModeNum::Comet => "Comet", AuraModeNum::Flash => "Flash", + AuraModeNum::Direct => "Direct", + AuraModeNum::Reactive => "Reactive", } } } @@ -317,6 +401,8 @@ impl From<&str> for AuraModeNum { "Pulse" => AuraModeNum::Pulse, "Comet" => AuraModeNum::Comet, "Flash" => AuraModeNum::Flash, + "Direct" => AuraModeNum::Direct, + "Reactive" => AuraModeNum::Reactive, _ => AuraModeNum::Static, } } @@ -336,6 +422,8 @@ impl From for AuraModeNum { 10 => AuraModeNum::Pulse, 11 => AuraModeNum::Comet, 12 => AuraModeNum::Flash, + 13 => AuraModeNum::Direct, + 14 => AuraModeNum::Reactive, _ => AuraModeNum::Static, } } @@ -359,6 +447,62 @@ impl From for AuraModeNum { } } +impl AuraModeNum { + /// Every mode the UI can offer. Index 9 is unused and is not included. + pub const fn all() -> [Self; 14] { + [ + Self::Static, + Self::Breathe, + Self::RainbowCycle, + Self::RainbowWave, + Self::Star, + Self::Rain, + Self::Highlight, + Self::Laser, + Self::Ripple, + Self::Pulse, + Self::Comet, + Self::Flash, + Self::Direct, + Self::Reactive, + ] + } + + /// Dynamic Lighting `effect` name for this mode. + /// + /// Names match the hid-asus firmware effect table advertised in + /// `effect_index` (`static`, `breathe`, `rainbow_cycle`, `rainbow_wave`, + /// `star`, `rain`, `highlight`, `laser`, `ripple`, `pulse`, `comet`, + /// `flash`, `reactive` on Xbox Ally X, plus `direct` when packed RGB is + /// available). + pub const fn to_dynamic_effect_str(&self) -> Option<&'static str> { + match self { + Self::Static => Some("static"), + Self::Breathe => Some("breathe"), + Self::RainbowCycle => Some("rainbow_cycle"), + Self::RainbowWave => Some("rainbow_wave"), + Self::Star => Some("star"), + Self::Rain => Some("rain"), + Self::Highlight => Some("highlight"), + Self::Laser => Some("laser"), + Self::Ripple => Some("ripple"), + Self::Pulse => Some("pulse"), + Self::Comet => Some("comet"), + Self::Flash => Some("flash"), + Self::Direct => Some("direct"), + Self::Reactive => Some("reactive"), + } + } + + /// True when the firmware effect consumes a direction attribute. + pub const fn uses_direction(self) -> bool { + matches!( + self, + Self::RainbowWave | Self::Laser | Self::Comet | Self::Rain | Self::Highlight + ) + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +548,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 +613,16 @@ impl AuraEffect { pub fn zone(&self) -> AuraZone { self.zone } + + /// Convert the effect colours to an array of RGB tuples for Dynamic Lighting palette. + pub fn to_dynamic_palette(&self) -> Vec<(u8, u8, u8)> { + let mut p = Vec::with_capacity(2); + p.push((self.colour1.r, self.colour1.g, self.colour1.b)); + if self.colour2.r != 0 || self.colour2.g != 0 || self.colour2.b != 0 { + p.push((self.colour2.r, self.colour2.g, self.colour2.b)); + } + p + } } impl Default for AuraEffect { @@ -539,9 +693,28 @@ impl From<&AuraEffect> for Vec { #[cfg(test)] mod tests { use crate::{ - AURA_LAPTOP_LED_MSG_LEN, AuraEffect, AuraModeNum, AuraZone, Colour, Direction, Speed, + AURA_LAPTOP_LED_MSG_LEN, AuraEffect, AuraModeNum, AuraZone, Colour, Direction, + LedBrightness, Speed, }; + #[test] + fn led_brightness_scales_for_dynamic_lighting() { + assert_eq!(LedBrightness::Off.to_scaled(255), 0); + assert_eq!(LedBrightness::Low.to_scaled(255), 85); + assert_eq!(LedBrightness::Med.to_scaled(255), 170); + assert_eq!(LedBrightness::High.to_scaled(255), 255); + + assert_eq!(LedBrightness::from_scaled(0, 255), LedBrightness::Off); + assert_eq!(LedBrightness::from_scaled(85, 255), LedBrightness::Low); + assert_eq!(LedBrightness::from_scaled(170, 255), LedBrightness::Med); + assert_eq!(LedBrightness::from_scaled(255, 255), LedBrightness::High); + + // Legacy 0..=3 path when max_brightness is small. + assert_eq!(LedBrightness::Med.to_scaled(3), 2); + assert_eq!(LedBrightness::from_scaled(2, 3), LedBrightness::Med); + assert_eq!(LedBrightness::High.to_scaled(3), 3); + } + #[test] fn check_led_static_packet() { let st = AuraEffect { @@ -689,4 +862,68 @@ mod tests { capture[..9] ); } + + #[test] + fn test_dynamic_lighting_conversions() { + assert_eq!(AuraModeNum::Static.to_dynamic_effect_str(), Some("static")); + assert_eq!( + AuraModeNum::Breathe.to_dynamic_effect_str(), + Some("breathe") + ); + assert_eq!( + AuraModeNum::RainbowCycle.to_dynamic_effect_str(), + Some("rainbow_cycle") + ); + assert_eq!( + AuraModeNum::RainbowWave.to_dynamic_effect_str(), + Some("rainbow_wave") + ); + assert_eq!(AuraModeNum::Pulse.to_dynamic_effect_str(), Some("pulse")); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), Some("flash")); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), Some("star")); + assert_eq!(AuraModeNum::Rain.to_dynamic_effect_str(), Some("rain")); + assert_eq!( + AuraModeNum::Highlight.to_dynamic_effect_str(), + Some("highlight") + ); + assert_eq!(AuraModeNum::Laser.to_dynamic_effect_str(), Some("laser")); + assert_eq!(AuraModeNum::Ripple.to_dynamic_effect_str(), Some("ripple")); + assert_eq!(AuraModeNum::Comet.to_dynamic_effect_str(), Some("comet")); + assert_eq!(AuraModeNum::Direct.to_dynamic_effect_str(), Some("direct")); + assert_eq!( + AuraModeNum::Reactive.to_dynamic_effect_str(), + Some("reactive") + ); + + assert_eq!(Speed::Low.to_dynamic_speed(), 0); + assert_eq!(Speed::Med.to_dynamic_speed(), 1); + assert_eq!(Speed::High.to_dynamic_speed(), 2); + + assert_eq!(Direction::Right.to_dynamic_direction_str(), "right"); + assert_eq!(Direction::Left.to_dynamic_direction_str(), "left"); + assert_eq!(Direction::Up.to_dynamic_direction_str(), "up"); + assert_eq!(Direction::Down.to_dynamic_direction_str(), "down"); + + let effect = AuraEffect { + colour1: Colour { + r: 0xff, + g: 0x10, + b: 0x20, + }, + colour2: Colour { + r: 0x00, + g: 0x30, + b: 0x40, + }, + ..Default::default() + }; + let palette = effect.to_dynamic_palette(); + assert_eq!( + palette, + vec![ + (0xff, 0x10, 0x20), + (0x00, 0x30, 0x40) + ] + ); + } } From d301a384959af0341ffbaea6585bf388f5ad6b23 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 11:53:44 +0100 Subject: [PATCH 3/6] feat(asusd): prefer Dynamic Lighting for Aura, Slash and AniMe Drive laptop Aura, Slash, and AniMe through asus::* Dynamic Lighting nodes when present. Keep hidraw/USB only when no valid DL node exists. Slash uses effect/speed/power_states under DL; logo fans out when asus::logo is registered. Signed-off-by: Marco Scardovi --- asusctl/examples/anime-diag.rs | 4 +- asusd/Cargo.toml | 2 +- asusd/src/aura_anime/mod.rs | 271 ++++++++-- asusd/src/aura_anime/trait_impls.rs | 147 +++-- asusd/src/aura_laptop/mod.rs | 771 ++++++++++++++++++++++++--- asusd/src/aura_laptop/trait_impls.rs | 269 ++++++---- asusd/src/aura_manager.rs | 273 +++++----- asusd/src/aura_slash/mod.rs | 40 +- asusd/src/aura_slash/trait_impls.rs | 251 +++++---- asusd/src/aura_types.rs | 182 +++++-- rog-dbus/src/zbus_slash.rs | 4 + rog-slash/src/data.rs | 13 + 12 files changed, 1602 insertions(+), 625 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(); diff --git a/asusd/Cargo.toml b/asusd/Cargo.toml index c9de52c2b..4a837f96d 100644 --- a/asusd/Cargo.toml +++ b/asusd/Cargo.toml @@ -16,7 +16,7 @@ path = "src/daemon.rs" [dependencies] config-traits = { path = "../config-traits" } rog_anime = { path = "../rog-anime", features = ["dbus"] } -rog_slash = { path = "../rog-slash", features = ["dbus"] } +rog_slash = { path = "../rog-slash", features = ["dbus", "detect"] } rog_aura = { path = "../rog-aura", features = ["dbus"] } rog_scsi = { path = "../rog-scsi", features = ["dbus"] } rog_platform = { path = "../rog-platform" } diff --git a/asusd/src/aura_anime/mod.rs b/asusd/src/aura_anime/mod.rs index 5d622f3ab..ff70c4292 100644 --- a/asusd/src/aura_anime/mod.rs +++ b/asusd/src/aura_anime/mod.rs @@ -9,12 +9,14 @@ use std::thread::sleep; use config_traits::StdConfig; use log::{debug, error, info, warn}; +use rog_anime::error::AnimeError; use rog_anime::usb::{ - Brightness, pkt_flush, pkt_set_brightness, pkt_set_enable_display, - pkt_set_enable_powersave_anim, pkts_for_init, + AnimAwake, AnimBooting, AnimShutdown, AnimSleeping, Brightness, pkt_flush, pkt_set_brightness, + pkt_set_builtin_animations, pkt_set_enable_display, pkt_set_enable_powersave_anim, + pkts_for_init, }; -use rog_anime::{ActionData, AnimeDataBuffer, AnimePacketType}; -use rog_platform::hid_raw::HidRaw; +use rog_anime::{ActionData, Animations, AnimeDataBuffer, AnimePacketType}; +use rog_platform::DynamicLed; use rog_platform::usb_raw::USBRaw; use tokio::sync::Mutex; @@ -23,8 +25,8 @@ use crate::error::RogError; #[derive(Debug, Clone)] pub struct AniMe { - hid: Option>>, usb: Option>>, + dynamic: Option>>, config: Arc>, cache: AniMeConfigCached, // set to force thread to exit @@ -35,13 +37,13 @@ pub struct AniMe { impl AniMe { pub fn new( - hid: Option>>, usb: Option>>, + dynamic: Option>>, config: Arc>, ) -> Self { Self { - hid, usb, + dynamic, config, cache: AniMeConfigCached::default(), thread_exit: Arc::new(AtomicBool::new(false)), @@ -49,6 +51,10 @@ impl AniMe { } } + pub fn has_dynamic_lighting(&self) -> bool { + self.dynamic.is_some() + } + /// Will fail if something is already holding the config lock async fn do_init_cache(&mut self) { if let Ok(mut config) = self.config.try_lock() { @@ -70,50 +76,179 @@ impl AniMe { /// Initialise the device if required. pub async fn do_initialization(&mut self) -> Result<(), RogError> { self.do_init_cache().await; + if self.has_dynamic_lighting() { + // hid-asus already ran AniMe init when registering asus::anime. + debug!("AniMe Dynamic Lighting ready (kernel owns init)"); + return Ok(()); + } let pkts = pkts_for_init(); - self.write_bytes(&pkts[0]).await?; - self.write_bytes(&pkts[1]).await?; + self.write_usb(&pkts[0]).await?; + self.write_usb(&pkts[1]).await?; debug!("Succesfully initialised AniMe matrix display"); Ok(()) } + async fn write_usb(&self, message: &[u8]) -> Result<(), RogError> { + let Some(usb) = &self.usb else { + return Err(RogError::Anime(AnimeError::NoDevice)); + }; + usb.lock().await.write_bytes(message)?; + Ok(()) + } + + /// Legacy USB packet write. Prefer the typed helpers when Dual-path aware. pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "AniMe Dynamic Lighting owns the device; USB packets are not used".into(), + )); } - Ok(()) + self.write_usb(message).await } - /// Write only a data packet. This will modify the leds brightness using the - /// global brightness set in config. - async fn write_data_buffer(&self, mut buffer: AnimeDataBuffer) -> Result<(), RogError> { - for led in buffer.data_mut().iter_mut() { - let mut bright = *led as f32; - if bright > 254.0 { - bright = 254.0; + pub async fn apply_brightness(&self, brightness: Brightness) -> Result<(), RogError> { + if let Some(led) = &self.dynamic { + let led = led.lock().await; + let off = brightness == Brightness::Off; + if led.has_enabled() { + led.set_lighting_enabled(!off)?; } - *led = bright as u8; + if !off || !led.has_enabled() { + let max = led.get_max_brightness().unwrap_or(3).max(1); + // AniMe Brightness is already 0..=3; scale only when the node is wider. + let level = if max <= 3 { + brightness as u8 + } else { + match brightness { + Brightness::Off => 0, + Brightness::Low => (max / 3).max(1), + Brightness::Med => ((max as u16 * 2) / 3) as u8, + Brightness::High => max, + } + }; + led.set_brightness(level)?; + } + return Ok(()); } - let data = AnimePacketType::try_from(buffer)?; - for row in &data { - self.write_bytes(row).await?; + self.write_usb(&pkt_set_brightness(brightness)).await?; + self.write_usb(&pkt_set_enable_display(brightness != Brightness::Off)) + .await + } + + pub async fn apply_display_enabled(&self, enabled: bool) -> Result<(), RogError> { + if let Some(led) = &self.dynamic { + let led = led.lock().await; + if led.has_enabled() { + led.set_lighting_enabled(enabled)?; + } + return Ok(()); } - self.write_bytes(&pkt_flush()).await + self.write_usb(&pkt_set_enable_display(enabled)).await + } + + pub async fn apply_builtin_animations(&self, settings: Animations) -> Result<(), RogError> { + if let Some(led) = &self.dynamic { + let led = led.lock().await; + // Each stage write updates only that firmware slot. + for name in [ + boot_effect_name(settings.boot), + awake_effect_name(settings.awake), + sleep_effect_name(settings.sleep), + shutdown_effect_name(settings.shutdown), + ] { + if led.supports_effect(name)? { + led.set_effect(name)?; + } + } + return Ok(()); + } + self.write_usb(&pkt_set_builtin_animations( + settings.boot, settings.awake, settings.sleep, settings.shutdown, + )) + .await } + /// Enable or disable builtin stage animations. + /// + /// `settings` is required when enabling so callers that already hold the + /// config lock can pass a copy instead of re-entering the mutex. pub async fn set_builtins_enabled( &self, enabled: bool, bright: Brightness, + settings: Animations, ) -> Result<(), RogError> { - self.write_bytes(&pkt_set_enable_powersave_anim(enabled)) + if let Some(led) = &self.dynamic { + let led = led.lock().await; + if enabled { + for name in [ + boot_effect_name(settings.boot), + awake_effect_name(settings.awake), + sleep_effect_name(settings.sleep), + shutdown_effect_name(settings.shutdown), + ] { + if led.supports_effect(name)? { + led.set_effect(name)?; + } + } + } else if led.supports_effect("direct")? { + led.set_effect("direct")?; + } + drop(led); + self.apply_brightness(bright).await?; + return Ok(()); + } + // Custom frames need powersave anims off without blanking the panel. + self.write_usb(&pkt_set_enable_powersave_anim(enabled)) .await?; - self.write_bytes(&pkt_set_enable_display(enabled)).await?; - self.write_bytes(&pkt_set_brightness(bright)).await?; - self.write_bytes(&pkt_set_enable_powersave_anim(enabled)) - .await + if enabled { + self.write_usb(&pkt_set_enable_display(true)).await?; + self.write_usb(&pkt_set_brightness(bright)).await?; + self.write_usb(&pkt_set_builtin_animations( + settings.boot, settings.awake, settings.sleep, settings.shutdown, + )) + .await?; + return self.write_usb(&pkt_set_enable_powersave_anim(true)).await; + } + self.write_usb(&pkt_set_brightness(bright)).await + } + + /// Write a direct intensity frame. Scales pixel values and uses DL + /// `direct_buffer` (1 byte/LED) when present. + pub(crate) async fn write_data_buffer( + &self, + mut buffer: AnimeDataBuffer, + ) -> Result<(), RogError> { + for led in buffer.data_mut().iter_mut() { + let mut bright = *led as f32; + if bright > 254.0 { + bright = 254.0; + } + *led = bright as u8; + } + + if let Some(led) = &self.dynamic { + let led = led.lock().await; + // Do not force the panel on: Off stays Off until brightness/enable. + if led.supports_effect("direct")? + && led.get_effect().ok().as_deref().map(str::trim) != Some("direct") + { + led.set_effect("direct")?; + } + let led_count = led.get_led_count()? as usize; + let data = buffer.data(); + if data.len() != led_count { + return Err(RogError::Anime(AnimeError::DataBufferLength)); + } + led.write_direct(data)?; + return Ok(()); + } + + let data = AnimePacketType::try_from(buffer)?; + for row in &data { + self.write_usb(row).await?; + } + self.write_usb(&pkt_flush()).await } /// Start an action thread. This is classed as a singleton and there should @@ -121,22 +256,26 @@ impl AniMe { /// /// Because this also writes to the usb device, other write tries (display /// only) *must* get the mutex lock and set the `thread_exit` atomic. - async fn run_thread(&self, actions: Vec, mut once: bool) { + pub async fn run_thread(&self, actions: Vec, mut once: bool) { if actions.is_empty() { warn!("AniMe system actions was empty"); return; } - self.write_bytes(&pkt_set_enable_powersave_anim(false)) - .await - .map_err(|err| { - warn!("rog_anime::run_animation:callback {}", err); - }) - .ok(); + let (bright, settings, anime_type) = { + let config = self.config.lock().await; + ( + config.display_brightness, + config.builtin_anims, + config.anime_type, + ) + }; + if let Err(err) = self.set_builtins_enabled(false, bright, settings).await { + warn!("rog_anime::run_animation: disable builtins {}", err); + } let thread_exit = self.thread_exit.clone(); let thread_running = self.thread_running.clone(); - let anime_type = self.config.lock().await.anime_type; let inner = self.clone(); // Loop rules: @@ -147,15 +286,9 @@ impl AniMe { // The only reason for this outer thread is to prevent blocking while waiting // for the next spawned thread to exit - // TODO: turn this in to async task (maybe? COuld still risk blocking main - // thread) tokio::spawn(async move { info!("AniMe new system thread started"); - // First two loops are to ensure we *do* aquire a lock on the mutex - // The reason the loop is required is because the USB writes can block - // for up to 10ms. We can't fail to get the atomics. while thread_running.load(Ordering::SeqCst) { - // Make any running loop exit first thread_exit.store(true, Ordering::SeqCst); } @@ -169,11 +302,10 @@ impl AniMe { } match action { ActionData::Animation(frames) => { - // TODO: sort all this out rog_anime::run_animation(frames, &|frame| { if thread_exit.load(Ordering::Acquire) { info!("rog-anime: animation sub-loop was asked to exit"); - return Ok(true); // Do safe exit + return Ok(true); } let inner = inner.clone(); tokio::task::spawn(async move { @@ -185,7 +317,7 @@ impl AniMe { }) .ok(); }); - Ok(false) // Don't exit yet + Ok(false) }); if thread_exit.load(Ordering::Acquire) { info!("rog-anime: sub-loop exited and main loop exiting now"); @@ -214,7 +346,6 @@ impl AniMe { break 'main; } } - // Clear the display on exit if let Ok(data) = AnimeDataBuffer::from_vec(anime_type, vec![0u8; anime_type.data_length()]) .map_err(|e| error!("{}", e)) @@ -227,17 +358,21 @@ impl AniMe { }) .ok(); } - // A write can block for many milliseconds so lets not hold the config lock for - // the same period - let enabled = inner.config.lock().await.builtin_anims_enabled; + let (enabled, bright, settings) = { + let config = inner.config.lock().await; + ( + config.builtin_anims_enabled, + config.display_brightness, + config.builtin_anims, + ) + }; inner - .write_bytes(&pkt_set_enable_powersave_anim(enabled)) + .set_builtins_enabled(enabled, bright, settings) .await .map_err(|err| { warn!("rog_anime::run_animation:callback {}", err); }) .ok(); - // Loop ended, set the atmonics thread_running.store(false, Ordering::SeqCst); info!("AniMe system thread exited"); }) @@ -246,3 +381,31 @@ impl AniMe { .ok(); } } + +fn boot_effect_name(anim: AnimBooting) -> &'static str { + match anim { + AnimBooting::GlitchConstruction => "boot_glitch_construction", + AnimBooting::StaticEmergence => "boot_static_emergence", + } +} + +fn awake_effect_name(anim: AnimAwake) -> &'static str { + match anim { + AnimAwake::BinaryBannerScroll => "awake_binary_banner_scroll", + AnimAwake::RogLogoGlitch => "awake_rog_logo_glitch", + } +} + +fn sleep_effect_name(anim: AnimSleeping) -> &'static str { + match anim { + AnimSleeping::BannerSwipe => "sleep_banner_swipe", + AnimSleeping::Starfield => "sleep_starfield", + } +} + +fn shutdown_effect_name(anim: AnimShutdown) -> &'static str { + match anim { + AnimShutdown::GlitchOut => "shutdown_glitch_out", + AnimShutdown::SeeYa => "shutdown_see_ya", + } +} diff --git a/asusd/src/aura_anime/trait_impls.rs b/asusd/src/aura_anime/trait_impls.rs index 337c868b5..06391f33c 100644 --- a/asusd/src/aura_anime/trait_impls.rs +++ b/asusd/src/aura_anime/trait_impls.rs @@ -3,10 +3,7 @@ use std::sync::atomic::Ordering; use config_traits::StdConfig; use log::{debug, error, warn}; use logind_zbus::manager::ManagerProxy; -use rog_anime::usb::{ - Brightness, pkt_set_brightness, pkt_set_builtin_animations, pkt_set_enable_display, - pkt_set_enable_powersave_anim, -}; +use rog_anime::usb::Brightness; use rog_anime::{Animations, AnimeDataBuffer, DeviceState}; use zbus::object_server::SignalEmitter; use zbus::proxy::CacheProperties; @@ -43,7 +40,6 @@ impl AniMeZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -60,24 +56,25 @@ impl AniMeZbus { } } -// None of these calls can be guarnateed to succeed unless we loop until okay -// If the try_lock *does* succeed then any other thread trying to lock will not -// grab it until we finish. #[interface(name = "xyz.ljones.Anime")] impl AniMeZbus { /// Writes a data stream of length. Will force system thread to exit until /// it is restarted async fn write(&self, input: AnimeDataBuffer) -> zbus::fdo::Result<()> { - let mut config = self.0.config.lock().await; - let bright = config.display_brightness; - if config.builtin_anims_enabled { - // This clears the display, causing flickers if done indiscriminately on every - // write. Therefore, we guard it behind a config check. - self.0.set_builtins_enabled(false, bright).await?; + let (bright, settings, disable_builtins) = { + let config = self.0.config.lock().await; + ( + config.display_brightness, + config.builtin_anims, + config.builtin_anims_enabled, + ) + }; + if disable_builtins { + self.0.set_builtins_enabled(false, bright, settings).await?; + let mut config = self.0.config.lock().await; config.builtin_anims_enabled = false; config.write(); } - drop(config); self.0.thread_exit.store(true, Ordering::SeqCst); self.0.write_data_buffer(input).await.map_err(|err| { warn!("ctrl_anime::run_animation:callback {}", err); @@ -99,14 +96,7 @@ impl AniMeZbus { #[zbus(property)] async fn set_brightness(&self, brightness: Brightness) { self.0 - .write_bytes(&pkt_set_brightness(brightness)) - .await - .map_err(|err| { - warn!("ctrl_anime::set_brightness {}", err); - }) - .ok(); - self.0 - .write_bytes(&pkt_set_enable_display(brightness != Brightness::Off)) + .apply_brightness(brightness) .await .map_err(|err| { warn!("ctrl_anime::set_brightness {}", err); @@ -131,10 +121,16 @@ impl AniMeZbus { /// animations" in Armory crate #[zbus(property)] async fn set_builtins_enabled(&self, enabled: bool) { - let mut config = self.0.config.lock().await; - let brightness = config.display_brightness; + let (brightness, settings, anime_type) = { + let config = self.0.config.lock().await; + ( + config.display_brightness, + config.builtin_anims, + config.anime_type, + ) + }; self.0 - .set_builtins_enabled(enabled, brightness) + .set_builtins_enabled(enabled, brightness, settings) .await .map_err(|err| { warn!("ctrl_anime::set_builtins_enabled {}", err); @@ -142,13 +138,12 @@ impl AniMeZbus { .ok(); if !enabled { - let anime_type = config.anime_type; let data = vec![255u8; anime_type.data_length()]; if let Ok(tmp) = AnimeDataBuffer::from_vec(anime_type, data).map_err(|err| { warn!("ctrl_anime::set_builtins_enabled {}", err); }) { self.0 - .write_bytes(tmp.data()) + .write_data_buffer(tmp) .await .map_err(|err| { warn!("ctrl_anime::set_builtins_enabled {}", err); @@ -157,6 +152,7 @@ impl AniMeZbus { } } + let mut config = self.0.config.lock().await; config.builtin_anims_enabled = enabled; config.write(); if enabled { @@ -175,24 +171,28 @@ impl AniMeZbus { /// Set which builtin animation is used for each stage #[zbus(property)] async fn set_builtin_animations(&self, settings: Animations) { + let brightness = { + let config = self.0.config.lock().await; + config.display_brightness + }; + // Apply the just-supplied settings (not a stale config snapshot). self.0 - .write_bytes(&pkt_set_builtin_animations( - settings.boot, settings.awake, settings.sleep, settings.shutdown, - )) + .apply_builtin_animations(settings) .await .map_err(|err| { - warn!("ctrl_anime::run_animation:callback {}", err); + warn!("ctrl_anime::set_builtin_animations {}", err); }) .ok(); self.0 - .write_bytes(&pkt_set_enable_powersave_anim(true)) + .set_builtins_enabled(true, brightness, settings) .await .map_err(|err| { - warn!("ctrl_anime::run_animation:callback {}", err); + warn!("ctrl_anime::set_builtin_animations {}", err); }) .ok(); let mut config = self.0.config.lock().await; config.display_enabled = true; + config.builtin_anims_enabled = true; config.builtin_anims = settings; config.write(); } @@ -209,10 +209,10 @@ impl AniMeZbus { #[zbus(property)] async fn set_enable_display(&self, enabled: bool) { self.0 - .write_bytes(&pkt_set_enable_display(enabled)) + .apply_display_enabled(enabled) .await .map_err(|err| { - warn!("ctrl_anime::run_animation:callback {}", err); + warn!("ctrl_anime::set_enable_display {}", err); }) .ok(); let mut config = self.0.config.lock().await; @@ -235,10 +235,10 @@ impl AniMeZbus { let pow = manager.on_external_power().await.unwrap_or_default(); self.0 - .write_bytes(&pkt_set_enable_display(!pow && !enabled)) + .apply_display_enabled(pow || !enabled) .await .map_err(|err| { - warn!("create_sys_event_tasks::off_when_lid_closed {}", err); + warn!("create_sys_event_tasks::off_when_unplugged {}", err); }) .ok(); @@ -278,7 +278,7 @@ impl AniMeZbus { let lid = manager.lid_closed().await.unwrap_or_default(); self.0 - .write_bytes(&pkt_set_enable_display(lid && !enabled)) + .apply_display_enabled(!(lid && enabled)) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_lid_closed {}", err); @@ -300,7 +300,6 @@ impl AniMeZbus { } /// Get the device state as stored by asusd - // #[zbus(property)] async fn device_state(&self) -> DeviceState { DeviceState::from(&*self.0.config.lock().await) } @@ -318,17 +317,14 @@ impl crate::CtrlTask for AniMeZbus { let inner4 = self.0.clone(); self.create_sys_event_tasks( move |sleeping| { - // on_sleep let inner = inner1.clone(); async move { let config = inner.config.lock().await.clone(); if config.display_enabled { - inner.thread_exit.store(true, Ordering::Release); // ensure clean slate + inner.thread_exit.store(true, Ordering::Release); inner - .write_bytes(&pkt_set_enable_display( - !(sleeping && config.off_when_suspended), - )) + .apply_display_enabled(!(sleeping && config.off_when_suspended)) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_suspended {}", err); @@ -337,28 +333,31 @@ impl crate::CtrlTask for AniMeZbus { if config.builtin_anims_enabled { inner - .write_bytes(&pkt_set_enable_powersave_anim( + .set_builtins_enabled( !(sleeping && config.off_when_suspended), - )) + config.display_brightness, + config.builtin_anims, + ) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_suspended {}", err); }) .ok(); } else if !sleeping && !config.builtin_anims_enabled { - // Run custom wake animation inner - .write_bytes(&pkt_set_enable_powersave_anim(false)) + .set_builtins_enabled( + false, + config.display_brightness, + config.builtin_anims, + ) .await - .ok(); // ensure builtins are disabled - + .ok(); inner.run_thread(inner.cache.wake.clone(), true).await; } } } }, move |shutting_down| { - // on_shutdown let inner = inner2.clone(); async move { let AniMeConfig { @@ -377,25 +376,28 @@ impl crate::CtrlTask for AniMeZbus { }, move |lid_closed| { let inner = inner3.clone(); - // on lid change async move { let AniMeConfig { off_when_lid_closed, builtin_anims_enabled, + builtin_anims, + display_brightness, .. } = *inner.config.lock().await; if off_when_lid_closed { if builtin_anims_enabled { inner - .write_bytes(&pkt_set_enable_powersave_anim(!lid_closed)) + .set_builtins_enabled( + !lid_closed, display_brightness, builtin_anims, + ) .await .map_err(|err| { - warn!("create_sys_event_tasks::off_when_suspended {}", err); + warn!("create_sys_event_tasks::off_when_lid_closed {}", err); }) .ok(); } inner - .write_bytes(&pkt_set_enable_display(!lid_closed)) + .apply_display_enabled(!lid_closed) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_lid_closed {}", err); @@ -406,26 +408,29 @@ impl crate::CtrlTask for AniMeZbus { }, move |power_plugged| { let inner = inner4.clone(); - // on power change async move { let AniMeConfig { off_when_unplugged, builtin_anims_enabled, + builtin_anims, brightness_on_battery, + display_brightness, .. } = *inner.config.lock().await; if off_when_unplugged { if builtin_anims_enabled { inner - .write_bytes(&pkt_set_enable_powersave_anim(power_plugged)) + .set_builtins_enabled( + power_plugged, display_brightness, builtin_anims, + ) .await .map_err(|err| { - warn!("create_sys_event_tasks::off_when_suspended {}", err); + warn!("create_sys_event_tasks::off_when_unplugged {}", err); }) .ok(); } inner - .write_bytes(&pkt_set_enable_display(power_plugged)) + .apply_display_enabled(power_plugged) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_unplugged {}", err); @@ -433,7 +438,7 @@ impl crate::CtrlTask for AniMeZbus { .ok(); } else { inner - .write_bytes(&pkt_set_brightness(brightness_on_battery)) + .apply_brightness(brightness_on_battery) .await .map_err(|err| { warn!("create_sys_event_tasks::off_when_unplugged {}", err); @@ -461,20 +466,11 @@ impl crate::Reloadable for AniMeZbus { .. } = *self.0.config.lock().await; - // Set builtins if builtin_anims_enabled { - self.0 - .write_bytes(&pkt_set_builtin_animations( - builtin_anims.boot, - builtin_anims.awake, - builtin_anims.sleep, - builtin_anims.shutdown, - )) - .await?; + self.0.apply_builtin_animations(builtin_anims).await?; } - // Builtins enabled or na? self.0 - .set_builtins_enabled(builtin_anims_enabled, display_brightness) + .set_builtins_enabled(builtin_anims_enabled, display_brightness, builtin_anims) .await?; let manager = get_logind_manager().await; @@ -484,7 +480,7 @@ impl crate::Reloadable for AniMeZbus { let turn_off = (lid_closed && off_when_lid_closed) || (!power_plugged && off_when_unplugged); self.0 - .write_bytes(&pkt_set_enable_display(!turn_off)) + .apply_display_enabled(!turn_off) .await .map_err(|err| { warn!("create_sys_event_tasks::reload {}", err); @@ -492,14 +488,13 @@ impl crate::Reloadable for AniMeZbus { .ok(); if turn_off || !display_enabled { - self.0.write_bytes(&pkt_set_enable_display(false)).await?; - // early return so we don't run animation thread + self.0.apply_display_enabled(false).await?; return Ok(()); } if !builtin_anims_enabled && !self.0.cache.boot.is_empty() { self.0 - .write_bytes(&pkt_set_enable_powersave_anim(false)) + .set_builtins_enabled(false, display_brightness, builtin_anims) .await .ok(); diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index cb3011b12..d3010b0dd 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -2,10 +2,14 @@ use std::sync::Arc; use config::AuraConfig; use config_traits::StdConfig; -use log::info; -use rog_aura::keyboard::{AuraLaptopUsbPackets, LedUsbPackets}; +use log::{debug, info}; +use rog_aura::keyboard::{AuraLaptopUsbPackets, AuraPowerState, LedUsbPackets}; use rog_aura::usb::{AURA_LAPTOP_LED_APPLY, AURA_LAPTOP_LED_SET}; use rog_aura::{AURA_LAPTOP_LED_MSG_LEN, AuraDeviceType, AuraEffect, LedBrightness, PowerZones}; +use rog_platform::DynamicLed; +use rog_platform::XboxLogoLed; +use rog_platform::dynamic_led::DIRECT_SAMPLE_RGB; +use rog_platform::error::PlatformError; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -17,12 +21,46 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Aura { + pub dynamic_kbd: Option>>, + pub dynamic_lightbar: Option>>, + pub dynamic_logo: Option>>, pub hid: Option>>, pub backlight: Option>>, + pub xbox_logo: Option>>, pub config: Arc>, } impl Aura { + #[must_use] + pub fn has_dynamic_lighting(&self) -> bool { + self.dynamic_kbd.is_some() || self.dynamic_lightbar.is_some() || self.dynamic_logo.is_some() + } + + /// Keyboard, lightbar, then logo. Each node is independently writable. + fn dynamic_nodes(&self) -> [Option<&Arc>>; 3] { + [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + self.dynamic_logo.as_ref(), + ] + } + + #[must_use] + pub fn has_dynamic_logo(&self) -> bool { + self.dynamic_logo.is_some() + } + + /// Prefer the keyboard `effect_index`; fall back to lightbar. + pub(crate) async fn effect_source(&self) -> Option> { + if let Some(kbd) = &self.dynamic_kbd { + Some(kbd.lock().await) + } else if let Some(lb) = &self.dynamic_lightbar { + Some(lb.lock().await) + } else { + None + } + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { Ok(()) @@ -35,14 +73,11 @@ impl Aura { /// Will lock the internal config and update. If anything else has locked /// this in scope then a deadlock can occur. pub async fn update_config(&self) -> Result<(), RogError> { + let bright = self.get_brightness().await; let mut config = self.config.lock().await; - let bright = if let Some(bl) = self.backlight.as_ref() { - bl.lock().await.get_brightness().unwrap_or_default() - } else { - config.brightness.into() - }; + let bright = bright.unwrap_or(config.brightness); config.read(); - config.brightness = bright.into(); + config.brightness = bright; config.write(); Ok(()) } @@ -73,6 +108,15 @@ impl Aura { self.write_effect_and_apply(config.led_type, &mode).await?; } } + // Under DL, keep the last whole-keyboard builtin alive when only a + // lightbar zone was updated into multizone storage. + if self.has_dynamic_lighting() + && let Some(kbd_fx) = config.builtins.get(&mode).cloned() + && kbd_fx.zone == rog_aura::AuraZone::None + { + self.write_effect_and_apply(config.led_type, &kbd_fx) + .await?; + } } else { let mode = config.current_mode; if let Some(effect) = config.builtins.get(&mode).cloned() { @@ -93,80 +137,330 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { - if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { - if let Some(platform) = &self.backlight { - platform - .lock() - .await - .set_kbd_rgb_mode(&tuf_kbd_rgb_mode_bytes(mode))?; + // Priority: Dynamic Lighting sysfs interface. + // Key1–4 map to the whole keyboard node; BarLeft/BarRight to lightbar; + // Logo maps to asus::logo when that node exists. + if self.has_dynamic_lighting() && !self.supports_zone(mode.zone) { + return Err(RogError::MissingFunction( + "Dynamic Lighting exposes keyboard/lightbar/logo topology, not legacy Aura subzones" + .into(), + )); + } + if self.has_dynamic_lighting() + && let Some(eff_str) = mode.mode.to_dynamic_effect_str() + { + let dir_str = mode.direction.to_dynamic_direction_str(); + let palette = mode.to_dynamic_palette(); + + let apply_to_led = |led: &DynamicLed| -> Result { + if !led.supports_effect(eff_str)? { + return Ok(false); + } + // Firmware effects do not commit while lighting is off. + if led.has_enabled() && map_dl_result(led.set_lighting_enabled(true))?.is_none() { + return Ok(false); + } + if led.has_speed() { + let speed = if led.has_speed_range() { + let (min, max) = + rog_platform::dynamic_led::parse_speed_range(&led.get_speed_range()?)?; + mode.speed.to_dynamic_speed_in_range(min, max) + } else { + mode.speed.to_dynamic_speed() + }; + if map_dl_result(led.set_supported_speed(speed))?.is_none() { + return Ok(false); + } + } + if mode.mode.uses_direction() + && led.has_direction() + && map_dl_result(led.set_supported_direction(dir_str))?.is_none() + { + return Ok(false); + } + if led.has_effects_palette() + && !palette.is_empty() + && map_dl_result(led.set_palette_colors(&palette))?.is_none() + { + return Ok(false); + } + Ok(map_dl_result(led.set_effect(eff_str))?.is_some()) + }; + + match mode.zone { + rog_aura::AuraZone::BarLeft | rog_aura::AuraZone::BarRight => { + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led)? { + return Ok(()); + } + } + } + rog_aura::AuraZone::Logo => { + if let Some(logo) = &self.dynamic_logo { + let led = logo.lock().await; + if apply_to_led(&led)? { + return Ok(()); + } + } + } + rog_aura::AuraZone::None => { + // Fan out to every chassis zone so each gets the same effect. + let mut any = false; + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + any |= apply_to_led(&kbd_led)?; + } + if let Some(lb) = &self.dynamic_lightbar { + let lb_led = lb.lock().await; + any |= apply_to_led(&lb_led)?; + } + if let Some(logo) = &self.dynamic_logo { + let logo_led = logo.lock().await; + any |= apply_to_led(&logo_led)?; + } + if any { + return Ok(()); + } + } + // Key1–4: whole keyboard node under DL (no legacy 4-zone split). + _ => { + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + if apply_to_led(&kbd_led)? { + return Ok(()); + } + } + } } + } + + // When Dynamic Lighting is active, do not fall back to raw hidraw or TUF + // platform + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "Dynamic lighting mode or zone not supported by kernel".to_string(), + )); + } + + // Fallback is selected only when no valid Dynamic Lighting node exists. + if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) + && let Some(platform) = &self.backlight + { + platform + .lock() + .await + .set_kbd_rgb_mode(&tuf_kbd_rgb_mode_bytes(mode))?; + return Ok(()); } else if let Some(hid_raw) = &self.hid { - // Some keyboard controllers (e.g. G533QS firmware) silently drop - // short HID writes and only honour packets matching the OUTPUT - // report size declared in the HID descriptor (64 bytes for the - // 0x5d report). Pad effect/SET/APPLY here so we keep working on - // newer Strix/Zephyrus models without regressing older laptops. - const PADDED_LEN: usize = 64; let bytes: [u8; AURA_LAPTOP_LED_MSG_LEN] = mode.into(); - let mut effect_padded = [0u8; PADDED_LEN]; - effect_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&bytes); - let mut set_padded = [0u8; PADDED_LEN]; - set_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_SET); - let mut apply_padded = [0u8; PADDED_LEN]; - apply_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_APPLY); let hid_raw = hid_raw.lock().await; - hid_raw.write_bytes(&effect_padded)?; - hid_raw.write_bytes(&set_padded)?; - // Changes won't persist unless apply is set - hid_raw.write_bytes(&apply_padded)?; - } else { - return Err(RogError::NoAuraKeyboard); + hid_raw.write_bytes(&pad_hid_report(&bytes))?; + hid_raw.write_bytes(&pad_hid_report(&AURA_LAPTOP_LED_SET))?; + hid_raw.write_bytes(&pad_hid_report(&AURA_LAPTOP_LED_APPLY))?; + return Ok(()); } - Ok(()) + Err(RogError::NoAuraKeyboard) } - pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + /// Read the current brightness as a 4-step [`LedBrightness`] level. + /// + /// Prefer the keyboard node. `enabled=false` is lighting-off and maps to + /// [`LedBrightness::Off`]; `brightness` alone is intensity and must not be + /// treated as off. Dynamic Lighting nodes may advertise `max_brightness > 3` + /// and are scaled back to Off/Low/Med/High. Chassis lightbar has no + /// separate dimmer and is not consulted. + pub async fn get_brightness(&self) -> Result { + if let Some(kbd) = &self.dynamic_kbd { + let led = kbd.lock().await; + if led.has_enabled() { + let Some(on) = map_dl_result(led.is_lighting_enabled())? else { + return Err(RogError::MissingFunction( + "No LED backlight control available".to_string(), + )); + }; + if !on { + return Ok(LedBrightness::Off); + } + } + let max = led.get_max_brightness()?; + let value = led.get_brightness()?; + return Ok(LedBrightness::from_scaled(value, max)); + } if let Some(backlight) = &self.backlight { - backlight.lock().await.set_brightness(value)?; - return Ok(()); + let value = backlight.lock().await.get_brightness()?; + return Ok(value.into()); } Err(RogError::MissingFunction( "No LED backlight control available".to_string(), )) } + /// Set keyboard/Aura brightness from a 4-step [`LedBrightness`] level. + /// + /// [`LedBrightness::Off`] uses `enabled=false` when the node advertises it. + /// Intensity is written only to the keyboard DL node (or legacy + /// `KeyboardBacklight`). Lightbar is gated with `enabled` only — hid-asus + /// has no separate lightbar dimmer. Xbox Guide LED tracks ring level. + pub async fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + let mut applied = false; + let off = brightness == LedBrightness::Off; + + if let Some(kbd) = &self.dynamic_kbd { + let led = kbd.lock().await; + if led.has_enabled() && map_dl_result(led.set_lighting_enabled(!off))?.is_some() { + applied = true; + } + if !off || !led.has_enabled() { + let max = led.get_max_brightness()?; + if map_dl_result(led.set_brightness(brightness.to_scaled(max)))?.is_some() { + applied = true; + } + } + } + + // Lightbar: enabled gates the zone; do not write brightness. + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if led.has_enabled() && map_dl_result(led.set_lighting_enabled(!off))?.is_some() { + applied = true; + } + } + + if let Some(backlight) = &self.backlight { + match backlight.lock().await.set_brightness(brightness.into()) { + Ok(()) => applied = true, + Err(e) if applied => { + debug!("WMI kbd backlight brightness sync failed: {e}"); + } + Err(e) => return Err(e.into()), + } + } + + if let Some(xbox) = &self.xbox_logo { + let led = xbox.lock().await; + let max = led.get_max_brightness().unwrap_or(3).max(1); + let level = if off { 0 } else { brightness.to_scaled(max) }; + if let Err(e) = led.set_brightness(level) { + debug!("Xbox Guide LED brightness sync failed: {e}"); + } + } + + if applied { + Ok(()) + } else { + Err(RogError::MissingFunction( + "No LED backlight control available".to_string(), + )) + } + } + /// Set combination state for boot animation/sleep animation/all leds/keys /// leds/side leds LED active pub async fn set_power_states(&self, config: &AuraConfig) -> Result<(), RogError> { - if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { - if let Some(backlight) = &self.backlight { - let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; + if self.has_dynamic_lighting() { + let mut applied = false; + let mut saw_power_states = false; + let mut unsupported_request = false; + // Resolve one flag set per physical node. Keyboard (and Ally / + // KeyboardAndLightbar / None) wins over Logo/Lid/RearGlow aliases so + // a disabled Keyboard sleep bit is not re-enabled by Logo. + let mut kbd_flags: Option> = None; + let mut lb_flags: Option> = None; + let mut kbd_from_primary = false; + for state in &config.enabled.states { + let requested = power_flags_for_state(state); + match state.zone { + PowerZones::Keyboard + | PowerZones::Ally + | PowerZones::KeyboardAndLightbar + | PowerZones::None => { + kbd_flags = Some(requested.clone()); + kbd_from_primary = true; + if matches!( + state.zone, + PowerZones::KeyboardAndLightbar | PowerZones::None + ) { + lb_flags = Some(requested); + } + } + PowerZones::Lightbar => lb_flags = Some(requested), + PowerZones::Logo | PowerZones::Lid | PowerZones::RearGlow => { + if !kbd_from_primary { + kbd_flags = Some(requested); + } + } + } + } + + let mut write_node = |led: &DynamicLed, requested: &[&str]| -> Result<(), RogError> { + if !led.has_power_states() { + return Ok(()); + } + saw_power_states = true; + let states = filter_power_states(requested, &led.get_power_states_index()?); + if states.is_empty() && !requested.is_empty() { + unsupported_request = true; + return Ok(()); + } + if map_dl_result(led.set_power_states_list(&states))?.is_some() { + applied = true; + } + Ok(()) + }; + + if let (Some(flags), Some(kbd)) = (&kbd_flags, &self.dynamic_kbd) { + write_node(&*kbd.lock().await, flags)?; + } + if let (Some(flags), Some(lb)) = (&lb_flags, &self.dynamic_lightbar) { + write_node(&*lb.lock().await, flags)?; + } + + if applied { + return Ok(()); + } + if !saw_power_states { + return Err(RogError::MissingFunction( + "Dynamic Lighting node does not expose power_states".into(), + )); + } + if unsupported_request { + return Err(RogError::MissingFunction( + "No requested power state is supported by this Dynamic Lighting node".into(), + )); } + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted power_states".into(), + )); + } + + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && let Some(backlight) = &self.backlight + { + // TODO: tuf bool array + let buf = config.enabled.to_bytes(config.led_type); + backlight.lock().await.set_kbd_rgb_state(&buf)?; } else if let Some(hid_raw) = &self.hid { let hid_raw = hid_raw.lock().await; if let Some(p) = config.enabled.states.first() && p.zone == PowerZones::Ally { - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xd1, 0x09, 0x01, p.new_to_byte() as u8, - 0x0, - 0x0, - ]; - hid_raw.write_bytes(&msg)?; + 0, + 0, + ])?; return Ok(()); } - let bytes = config.enabled.to_bytes(config.led_type); - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], - ]; - hid_raw.write_bytes(&msg)?; + ])?; } Ok(()) } @@ -184,60 +478,239 @@ impl Aura { config.write(); } - let pkt_type = effect[0][1]; - const PER_KEY_TYPE: u8 = 0xbc; + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && !self.has_dynamic_lighting() + && let Some(tuf) = &self.backlight + { + for row in effect.iter() { + let rgb = row.get(9..12).ok_or(PlatformError::InvalidValue)?; + let [r, g, b] = rgb else { + return Err(PlatformError::InvalidValue.into()); + }; + tuf.lock().await.set_kbd_rgb_mode(&[ + 0, 0, *r, *g, *b, 0, + ])?; + } + return Ok(()); + } + + for maybe_led in self.dynamic_nodes().into_iter().flatten() { + let dynamic = maybe_led.lock().await; + let led_count = dynamic.get_led_count()? as usize; + let sample = dynamic.direct_sample_size(); + if sample != DIRECT_SAMPLE_RGB { + debug!( + "Skipping direct Aura frame on '{}': expected RGB stride, got {sample}", + dynamic.name() + ); + continue; + } + let rgb_buf = match direct_rgb_payload(effect, led_count) { + Ok(buf) => buf, + Err(PlatformError::InvalidValue) => { + debug!( + "Skipping direct Aura frame on '{}': LED count mismatch (node has {led_count})", + dynamic.name() + ); + continue; + } + Err(e) => return Err(e.into()), + }; + if rgb_buf.is_empty() { + continue; + } + // Kernel direct_buffer is meaningful only while effect=direct and + // the zone is enabled; otherwise firmware effects keep owning the + // controller and frames are ignored or overwritten. + if map_dl_result(prepare_direct_streaming(&dynamic))?.is_none() { + debug!( + "Dynamic lighting node '{}' busy preparing direct (-EBUSY), trying alternate", + dynamic.name() + ); + continue; + } + if map_dl_result(dynamic.write_direct(&rgb_buf))?.is_none() { + debug!( + "Dynamic lighting node '{}' busy (-EBUSY), trying alternate", + dynamic.name() + ); + continue; + } + config.per_key_mode_active = true; + return Ok(()); + } + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted the direct frame".into(), + )); + } if let Some(hid_raw) = &self.hid { + let first = effect.first().ok_or(PlatformError::InvalidValue)?; + let packet_type = *first.get(1).ok_or(PlatformError::InvalidValue)?; + if effect.iter().any(|row| { + row.first() != Some(&0x5d) + || row.len() > 64 + || (packet_type == 0xbc && row.len() != 64) + }) { + return Err(PlatformError::InvalidValue.into()); + } let hid_raw = hid_raw.lock().await; - if pkt_type != PER_KEY_TYPE { + if packet_type != 0xbc { config.per_key_mode_active = false; - hid_raw.write_bytes(&effect[0])?; + hid_raw.write_bytes(first)?; hid_raw.write_bytes(&AURA_LAPTOP_LED_SET)?; - // hid_raw.write_bytes(&LED_APPLY)?; } else { if !config.per_key_mode_active { - let init = LedUsbPackets::get_init_msg(); - hid_raw.write_bytes(&init)?; + hid_raw.write_bytes(&LedUsbPackets::get_init_msg())?; config.per_key_mode_active = true; } - for row in effect.iter() { + for row in effect { hid_raw.write_bytes(row)?; } } - } else if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) - && let Some(tuf) = &self.backlight - { - for row in effect.iter() { - let r = row[9]; - let g = row[10]; - let b = row[11]; - tuf.lock().await.set_kbd_rgb_mode(&[ - 0, 0, r, g, b, 0, - ])?; - } + return Ok(()); } - Ok(()) + + Err(RogError::NoAuraKeyboard) } + /// One-shot Ally power enable. Under Dynamic Lighting the kernel owns + /// Ally LED power via `power_states` (`d1 09`); mark the config fixed + /// without hidraw. Legacy hidraw path keeps the old `5d bd` write. 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); + let needs_fix = { + let config = self.config.lock().await; + config.led_type == AuraDeviceType::Ally && config.ally_fix.is_none() + }; + if !needs_fix { + return Ok(()); + } + + if self.has_dynamic_lighting() { + let config_snapshot = self.config.lock().await.clone(); + if let Err(e) = self.set_power_states(&config_snapshot).await { + debug!("Ally Dynamic Lighting power_states on first fix: {e}"); } + let mut config = self.config.lock().await; + config.ally_fix = Some(true); + config.write(); + return Ok(()); + } + + if let Some(hid_raw) = &self.hid { + hid_raw.lock().await.write_bytes(&[ + 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, + ])?; + let mut config = self.config.lock().await; + config.ally_fix = Some(true); config.write(); } Ok(()) } } +fn direct_rgb_payload( + packets: &AuraLaptopUsbPackets, + led_count: usize, +) -> Result, PlatformError> { + let expected = led_count + .checked_mul(3) + .ok_or(PlatformError::InvalidValue)?; + let first = packets.first().ok_or(PlatformError::InvalidValue)?; + let mut rgb = Vec::with_capacity(expected); + if packets.len() == 1 && led_count == 4 { + rgb.extend_from_slice(first.get(9..21).ok_or(PlatformError::InvalidValue)?); + } else { + // Byte 6 high nibble is the key-group index; place each payload at + // group*16 LEDs so out-of-order packets do not append wrongly. + let mut frame = vec![0u8; expected]; + let mut filled = 0usize; + for packet in packets { + if packet.get(1).copied() != Some(0xbc) { + return Err(PlatformError::InvalidValue); + } + let group = (*packet.get(6).ok_or(PlatformError::InvalidValue)? >> 4) as usize; + let count = *packet.get(7).ok_or(PlatformError::InvalidValue)? as usize; + let payload_len = count.checked_mul(3).ok_or(PlatformError::InvalidValue)?; + let start = group + .checked_mul(16) + .and_then(|leds| leds.checked_mul(3)) + .ok_or(PlatformError::InvalidValue)?; + let end = start + .checked_add(payload_len) + .ok_or(PlatformError::InvalidValue)?; + if end > frame.len() { + return Err(PlatformError::InvalidValue); + } + let src_end = 9usize + .checked_add(payload_len) + .ok_or(PlatformError::InvalidValue)?; + frame[start..end] + .copy_from_slice(packet.get(9..src_end).ok_or(PlatformError::InvalidValue)?); + filled = filled.saturating_add(payload_len); + } + if filled != expected { + return Err(PlatformError::InvalidValue); + } + rgb = frame; + } + if rgb.len() != expected { + return Err(PlatformError::InvalidValue); + } + Ok(rgb) +} + +pub(super) fn supports_dynamic_zone(zone: rog_aura::AuraZone) -> bool { + matches!( + zone, + rog_aura::AuraZone::None + | rog_aura::AuraZone::Key1 + | rog_aura::AuraZone::Key2 + | rog_aura::AuraZone::Key3 + | rog_aura::AuraZone::Key4 + | rog_aura::AuraZone::BarLeft + | rog_aura::AuraZone::BarRight + | rog_aura::AuraZone::Logo + ) +} + +impl Aura { + fn supports_zone(&self, zone: rog_aura::AuraZone) -> bool { + match zone { + rog_aura::AuraZone::Logo => self.has_dynamic_logo(), + other => supports_dynamic_zone(other), + } + } +} + +/// `Ok(None)` when the node is inactive (`-EBUSY`). Other errors propagate. +fn map_dl_result(result: Result) -> Result, RogError> { + match result { + Ok(value) => Ok(Some(value)), + Err(err) => { + let err = RogError::from(err); + if is_dl_node_inactive(&err) { + Ok(None) + } else { + Err(err) + } + } + } +} + +/// Kernel may return `-EBUSY` when a Dynamic Lighting node briefly rejects a +/// write (for example while another zone owns a shared controller path). +fn is_dl_node_inactive(err: &RogError) -> bool { + let io = match err { + RogError::Platform(PlatformError::IoPath(_, e)) => Some(e), + RogError::Platform(PlatformError::Io(e)) => Some(e), + RogError::Write(_, e) | RogError::Path(_, e) | RogError::Io(e) => Some(e), + _ => None, + }; + io.is_some_and(|e| e.kind() == std::io::ErrorKind::ResourceBusy || e.raw_os_error() == Some(16)) +} + /// Build the `cmd mode r g b speed` values for the asus-wmi `kbd_rgb_mode` /// sysfs attribute used by TUF laptops. /// @@ -254,3 +727,141 @@ fn tuf_kbd_rgb_mode_bytes(mode: &AuraEffect) -> [u8; 6] { mode.speed.into(), ] } + +fn pad_hid_report(bytes: &[u8]) -> [u8; 64] { + let mut padded = [0u8; 64]; + if let Some(slot) = padded.get_mut(..bytes.len()) { + slot.copy_from_slice(bytes); + } + padded +} + +fn filter_power_states<'a>(requested: &[&'a str], supported: &str) -> Vec<&'a str> { + let supported: std::collections::HashSet<_> = supported.split_whitespace().collect(); + requested + .iter() + .copied() + .filter(|state| supported.contains(state)) + .collect() +} + +fn power_flags_for_state(state: &AuraPowerState) -> Vec<&'static str> { + let mut requested = Vec::new(); + if state.boot { + requested.push("boot"); + } + if state.awake { + requested.push("awake"); + } + if state.sleep { + requested.push("sleep"); + } + if state.shutdown { + requested.push("shutdown"); + } + requested +} + +/// Enable the zone and select kernel `direct` before streaming RGB frames. +fn prepare_direct_streaming(led: &DynamicLed) -> Result<(), PlatformError> { + if led.has_enabled() { + led.set_lighting_enabled(true)?; + } + if led.supports_effect("direct")? + && led.get_effect().ok().as_deref().map(str::trim) != Some("direct") + { + led.set_effect("direct")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::{Error, ErrorKind}; + + use rog_aura::keyboard::AuraPowerState; + use rog_aura::{AuraZone, PowerZones}; + use rog_platform::error::PlatformError; + + use super::{ + direct_rgb_payload, filter_power_states, is_dl_node_inactive, power_flags_for_state, + supports_dynamic_zone, + }; + use crate::error::RogError; + + #[test] + fn direct_payload_requires_exact_size() { + let mut packet = vec![0; 15]; + packet[1] = 0xbc; + packet[7] = 2; + packet[9..15].copy_from_slice(&[ + 1, 2, 3, 4, 5, 6, + ]); + assert_eq!( + direct_rgb_payload(&vec![packet.clone()], 2).unwrap().len(), + 6 + ); + assert!(direct_rgb_payload(&vec![packet], 3).is_err()); + assert!(direct_rgb_payload(&Vec::new(), 1).is_err()); + } + + #[test] + fn power_states_are_intersected_with_capabilities() { + assert_eq!( + filter_power_states( + &[ + "boot", "awake", "shutdown" + ], + "awake sleep" + ), + ["awake"] + ); + } + + #[test] + fn power_flags_follow_individual_zone_state() { + let kbd = AuraPowerState { + zone: PowerZones::Keyboard, + boot: true, + awake: false, + sleep: false, + shutdown: false, + }; + let lb = AuraPowerState { + zone: PowerZones::Lightbar, + boot: false, + awake: true, + sleep: true, + shutdown: false, + }; + assert_eq!(power_flags_for_state(&kbd), ["boot"]); + assert_eq!( + power_flags_for_state(&lb), + [ + "awake", "sleep" + ] + ); + } + + #[test] + fn dynamic_topology_allows_keyboard_and_lightbar_proxies() { + assert!(supports_dynamic_zone(AuraZone::None)); + assert!(supports_dynamic_zone(AuraZone::Key1)); + assert!(supports_dynamic_zone(AuraZone::BarLeft)); + assert!(supports_dynamic_zone(AuraZone::Logo)); + } + + #[test] + fn inactive_dl_node_detects_ebusy() { + let err = RogError::Platform(PlatformError::IoPath( + "asus::keyboard/brightness".into(), + Error::new(ErrorKind::ResourceBusy, "Device or resource busy"), + )); + assert!(is_dl_node_inactive(&err)); + let errno = RogError::Platform(PlatformError::IoPath( + "asus::keyboard/direct_buffer".into(), + Error::from_raw_os_error(16), + )); + assert!(is_dl_node_inactive(&errno)); + } +} diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 90629044d..1899795e8 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -9,7 +9,7 @@ use zbus::object_server::SignalEmitter; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; -use super::Aura; +use super::{Aura, supports_dynamic_zone}; use crate::error::RogError; use crate::{CtrlTask, Reloadable}; @@ -27,23 +27,34 @@ impl AuraZbus { pub async fn start_tasks( mut self, connection: &Connection, - // _signal_ctx: SignalEmitter<'static>, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); - // let signal_ctx = signal_ctx.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); + + // Wire logind sleep/resume (and related) handlers. Without this, Aura never + // re-applies power states / effects after s2idle and keyboard LEDs stay off. + match SignalEmitter::new(connection, path.as_ref()).map(|s| s.to_owned()) { + Ok(signal) => { + if let Err(e) = self.create_tasks(signal).await { + warn!("Aura CtrlTask (sleep/resume) setup failed: {e}"); + } + } + Err(e) => { + warn!("Aura could not create SignalEmitter for CtrlTask: {e}"); + } + } + connection .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - // TODO: skip this until we keep handles to tasks so they can be killed - // task.create_tasks(signal_ctx).await - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } @@ -61,25 +72,17 @@ impl AuraZbus { /// Return the current LED brightness #[zbus(property)] async fn brightness(&self) -> Result { - if let Some(bl) = self.0.backlight.as_ref() { - return Ok(bl.lock().await.get_brightness().map(|n| n.into())?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + Ok(self.0.get_brightness().await?) } - /// Set the keyboard brightness level (0-3) + /// Set the keyboard brightness level (Off/Low/Med/High) #[zbus(property)] async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { - if let Some(bl) = self.0.backlight.as_ref() { - let res = bl.lock().await.set_brightness(brightness.into()); - if res.is_ok() { - let mut config = self.0.config.lock().await; - config.brightness = brightness; - config.write(); - } - return Ok(res?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + self.0.set_brightness(brightness).await?; + let mut config = self.0.config.lock().await; + config.brightness = brightness; + config.write(); + Ok(()) } /// Total levels of brightness available @@ -96,18 +99,57 @@ impl AuraZbus { /// The total available modes #[zbus(property)] async fn supported_basic_modes(&self) -> Result, ZbErr> { - let config = self.0.config.lock().await; + let mut config = self.0.config.lock().await; + if self.0.has_dynamic_lighting() + && let Some(led) = self.0.effect_source().await + { + let mut modes = Vec::new(); + let mut dirty = false; + for mode in AuraModeNum::all() { + if let Some(eff_str) = mode.to_dynamic_effect_str() + && led.supports_effect(eff_str)? + { + modes.push(mode); + if let std::collections::btree_map::Entry::Vacant(slot) = + config.builtins.entry(mode) + { + slot.insert(AuraEffect::default_with_mode(mode)); + dirty = true; + } + } + } + if dirty { + config.write(); + } + return Ok(modes); + } Ok(config.builtins.keys().cloned().collect()) } #[zbus(property)] async fn supported_basic_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + // The kernel nodes split keyboard from lightbar, but do not model + // the historical four keyboard or left/right lightbar zones. + return Ok(Vec::new()); + } let config = self.0.config.lock().await; Ok(config.support_data.basic_zones.clone()) } #[zbus(property)] async fn supported_power_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + let has_power_states = self + .0 + .effect_source() + .await + .is_some_and(|led| led.has_power_states()); + if !has_power_states { + // Avoid advertising zones the UI cannot actually control. + return Ok(Vec::new()); + } + } let config = self.0.config.lock().await; Ok(config.support_data.power_zones.clone()) } @@ -116,8 +158,6 @@ impl AuraZbus { #[zbus(property)] async fn led_mode(&self) -> Result { // entirely possible to deadlock here, so use try instead of lock() - // let ctrl = self.0.lock().await; - // Ok(config.current_mode) if let Ok(config) = self.0.config.try_lock() { Ok(config.current_mode) } else { @@ -137,7 +177,7 @@ impl AuraZbus { if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if let Err(e) = self.0.set_brightness(config.brightness.into()).await { + if let Err(e) = self.0.set_brightness(config.brightness).await { log::warn!("Could not set keyboard backlight brightness: {e}"); } config.write(); @@ -166,10 +206,47 @@ impl AuraZbus { #[zbus(property)] async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> { let mut config = self.0.config.lock().await; - if !config.support_data.basic_modes.contains(&effect.mode) - || effect.zone != AuraZone::None - && !config.support_data.basic_zones.contains(&effect.zone) - { + let (is_mode_supported, is_zone_supported) = if self.0.has_dynamic_lighting() { + let mode_ok = if let Some(eff_str) = effect.mode.to_dynamic_effect_str() { + match effect.zone { + AuraZone::BarLeft | AuraZone::BarRight => { + if let Some(lb) = &self.0.dynamic_lightbar { + lb.lock().await.supports_effect(eff_str)? + } else { + false + } + } + AuraZone::Logo => { + if let Some(logo) = &self.0.dynamic_logo { + logo.lock().await.supports_effect(eff_str)? + } else { + false + } + } + _ => match self.0.effect_source().await { + Some(led) => led.supports_effect(eff_str)?, + None => false, + }, + } + } else { + false + }; + let zone_ok = match effect.zone { + AuraZone::Logo => self.0.has_dynamic_logo(), + other => supports_dynamic_zone(other), + }; + (mode_ok, zone_ok) + } else { + ( + config.support_data.basic_modes.contains(&effect.mode), + effect.zone == AuraZone::None + || config.support_data.basic_zones.contains(&effect.zone) + || (self.0.dynamic_lightbar.is_some() + && matches!(effect.zone, AuraZone::BarLeft | AuraZone::BarRight)), + ) + }; + + if !is_mode_supported || !is_zone_supported { return Err(ZbErr::NotSupported(format!( "The Aura effect is not supported: {effect:?}" ))); @@ -181,7 +258,7 @@ impl AuraZbus { if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if let Err(e) = self.0.set_brightness(config.brightness.into()).await { + if let Err(e) = self.0.set_brightness(config.brightness).await { log::warn!("Could not set keyboard backlight brightness: {e}"); } config.set_builtin(effect); @@ -246,57 +323,25 @@ impl CtrlTask for AuraZbus { self.create_sys_event_tasks( move |sleeping| { let inner1 = inner1.clone(); - // unwrap as we want to bomb out of the task async move { - if !sleeping { - info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner1.backlight { - backlight - .lock() - .await - .set_brightness(inner1.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if sleeping { + if let Err(e) = inner1.update_config().await { + error!("CtrlKbdLedTask: persist config before sleep: {e}"); } - let mut config = inner1.config.lock().await; - inner1 - .write_current_config_mode(&mut config) - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); - } else if sleeping { - inner1 - .update_config() - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + return; } + + // Dynamic Lighting / HID nodes can lag behind s2idle resume. + restore_aura_after_resume(&inner1).await; } }, move |_shutting_down| { let inner3 = inner3.clone(); async move { - info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner3.backlight { - // unwrap as we want to bomb out of the task - backlight - .lock() - .await - .set_brightness(inner3.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + info!("CtrlKbdLedTask restoring brightness on shutdown path"); + let brightness = inner3.config.lock().await.brightness; + if let Err(e) = inner3.set_brightness(brightness).await { + error!("CtrlKbdLedTask: {e}"); } } }, @@ -311,41 +356,59 @@ impl CtrlTask for AuraZbus { ) .await; - // let ctrl2 = self.0.clone(); - // let ctrl = self.0.lock().await; - // if ctrl.led_node.has_brightness_control() { - // let watch = ctrl.led_node.monitor_brightness()?; - // tokio::spawn(async move { - // let mut buffer = [0; 32]; - // watch - // .into_event_stream(&mut buffer) - // .unwrap() - // .for_each(|_| async { - // if let Some(lock) = ctrl2.try_lock() { - // load_save(true, lock).unwrap(); // unwrap as we want - // // to - // // bomb out of the - // // task - // } - // }) - // .await; - // }); - // } - Ok(()) } } +/// Re-apply power states, brightness, and the current effect after resume. +/// +/// Matches [`Reloadable::reload`] ordering, with a short settle delay and one +/// retry so Dynamic Lighting sysfs is ready after s2idle. +async fn restore_aura_after_resume(ctrl: &Aura) { + info!("CtrlKbdLedTask restoring Aura after resume"); + const SETTLE_MS: u64 = 500; + for attempt in 1..=2u8 { + tokio::time::sleep(std::time::Duration::from_millis(SETTLE_MS)).await; + + let mut config = ctrl.config.lock().await; + if let Err(e) = ctrl.set_power_states(&config).await { + // Missing power_states on DL is expected; still try brightness/mode. + warn!("CtrlKbdLedTask: restore power states (attempt {attempt}): {e}"); + } + let brightness = config.brightness; + if let Err(e) = ctrl.set_brightness(brightness).await { + warn!("CtrlKbdLedTask: restore brightness (attempt {attempt}): {e}"); + } + match ctrl.write_current_config_mode(&mut config).await { + Ok(()) => { + info!("CtrlKbdLedTask: Aura restored after resume"); + return; + } + Err(e) => { + error!("CtrlKbdLedTask: restore mode (attempt {attempt}): {e}"); + } + } + } +} + impl Reloadable for AuraZbus { async fn reload(&mut self) -> Result<(), RogError> { self.0.fix_ally_power().await?; + let brightness = { + let config = self.0.lock_config().await; + debug!("reloading power states"); + self.0 + .set_power_states(&config) + .await + .map_err(|err| warn!("{err}")) + .ok(); + config.brightness + }; + debug!("reloading brightness"); + if let Err(e) = self.0.set_brightness(brightness).await { + warn!("Could not restore Aura brightness on reload: {e}"); + } let mut config = self.0.lock_config().await; - debug!("reloading power states"); - self.0 - .set_power_states(&config) - .await - .map_err(|err| warn!("{err}")) - .ok(); debug!("reloading keyboard mode"); self.0.write_current_config_mode(&mut config).await?; Ok(()) diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d809bd744..8cfb18fe7 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -4,12 +4,12 @@ // - Add it to Zbus server // - If udev sees device removed then remove the zbus path -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::sync::Arc; -use 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; @@ -97,7 +97,6 @@ pub struct AsusDevice { pub struct DeviceManager { _dbus_connection: Connection, - _hid_handles: Arc>>>>, } /// Returns true if this hidraw device is a non-Aura interface on the @@ -132,30 +131,9 @@ fn is_non_aura_1ce6_interface(device: &Device) -> bool { } impl DeviceManager { - #[allow(clippy::type_complexity)] - async fn get_or_create_hid_handle( - handles: &Arc>>>>, - endpoint: &Device, - ) -> Result<(Arc>, String), RogError> { - let dev_node = endpoint - .devnode() - .ok_or_else(|| RogError::MissingFunction("hidraw devnode missing".to_string()))?; - let key = dev_node.to_string_lossy().to_string(); - - if let Some(existing) = handles.lock().await.get(&key).cloned() { - return Ok((existing, key)); - } - - let hidraw = HidRaw::from_device(endpoint.clone())?; - let handle = Arc::new(Mutex::new(hidraw)); - handles.lock().await.insert(key.clone(), handle.clone()); - Ok((handle, key)) - } - async fn init_hid_devices( connection: &Connection, device: Device, - handles: Arc>>>>, ) -> Result, RogError> { let mut devices = Vec::new(); if let Some(usb_device) = device.parent_with_subsystem_devtype("usb", "usb_device")? @@ -170,63 +148,61 @@ impl DeviceManager { // So let's see what we have and: // 1. Generate an interface path // 2. Create the device - // Use the top-level endpoint, not the parent - if let Ok((dev, hid_key)) = Self::get_or_create_hid_handle(&handles, &device).await { - debug!("Testing device {usb_id:?}"); - // SLASH DEVICE - if let Ok(dev_type) = - DeviceHandle::new_slash_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Slash(slash) = dev_type.clone() - { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); - let ctrl = SlashZbus::new(slash); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start Slash tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } - } - // ANIME MATRIX DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_anime_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::AniMe(anime) = dev_type.clone() + let usb_id_str = usb_id.to_str().unwrap_or_default(); + let hid_node = device + .devnode() + .map(|path| path.to_string_lossy().into_owned()); + let hid = HidRaw::from_device(device) + .map(|hid| Arc::new(Mutex::new(hid))) + .ok(); + + // Legacy Slash HID when sysfs asus::slash is absent. + if let Some(hid_dev) = hid.clone() + && let Ok(dev_type) = DeviceHandle::new_slash_hid(hid_dev, usb_id_str).await + && let DeviceHandle::Slash(slash) = &dev_type + { + let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); + let ctrl = SlashZbus::new(slash.clone()); + if ctrl + .start_tasks(connection, path.clone()) + .await + .map_err(|e| { + error!("Failed to start Slash tasks: {e:?}, not adding this device") + }) + .is_ok() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_anime()); - let ctrl = AniMeZbus::new(anime); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start AniMe tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } + devices.push(AsusDevice { + device: dev_type, + dbus_path: path, + hid_key: hid_node.clone(), + }); } - // AURA LAPTOP DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Aura(aura) = dev_type.clone() + return Ok(devices); + } + + let aura_type = AuraDeviceType::from(usb_id_str); + if matches!( + aura_type, + AuraDeviceType::LaptopKeyboard2021 + | AuraDeviceType::LaptopKeyboardPre2021 + | AuraDeviceType::LaptopKeyboardTuf + | AuraDeviceType::Ally + ) { + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(hid, usb_id_str).await + && let DeviceHandle::Aura(aura) = &dev_type { + // Dynamic Lighting owns the effect path and drops hidraw. + // Do not key hotplug teardown on the probe hidraw node, or a + // remove uevent (s2idle / re-enumeration) would unregister + // the D-Bus Aura object while sysfs DL nodes remain valid. + let hid_key = if aura.hid.is_some() { hid_node } else { None }; let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); - let ctrl = AuraZbus::new(aura); + // One Aura owner per D-Bus path (USB re-enumeration / hotplug). + if devices.iter().any(|d| d.dbus_path == path) { + debug!("Aura path {path} already registered, skipping duplicate"); + return Ok(devices); + } + let ctrl = AuraZbus::new(aura.clone()); if ctrl .start_tasks(connection, path.clone()) .await @@ -238,22 +214,18 @@ impl DeviceManager { devices.push(AsusDevice { device: dev_type, dbus_path: path, - hid_key: Some(hid_key), + hid_key, }); } } - } else { - warn!("Failed to initialise shared hid handle for {usb_id:?}"); + return Ok(devices); } } Ok(devices) } /// To be called on daemon startup - async fn init_all_hid( - connection: &Connection, - handles: Arc>>>>, - ) -> Result, RogError> { + async fn init_all_hid(connection: &Connection) -> Result, RogError> { // Ensure we only process one hidraw interface per physical USB device. // A USB device can expose multiple HID interfaces (and thus multiple hidraw nodes). // Processing more than one causes duplicate device initialisation which can @@ -284,13 +256,24 @@ impl DeviceManager { if let Ok(Some(usb_parent)) = device.parent_with_subsystem_devtype("usb", "usb_device") { let parent_path = usb_parent.syspath().to_string_lossy().to_string(); + // Skip non-Aura interfaces without claiming the parent, so a later + // RGB-capable hidraw on the same USB device can still probe. + let aura_capable = HidRaw::supports_output_report(&device, 0x5d) + || HidRaw::supports_output_report(&device, 0x5e); + if !aura_capable { + debug!( + "Skipping non-Aura hidraw interface {}", + device.syspath().to_string_lossy() + ); + continue; + } if !seen_usb_parents.insert(parent_path) { debug!("Skipping duplicate ASUS hidraw for USB parent already processed"); continue; } } - devices.append(&mut Self::init_hid_devices(connection, device, handles.clone()).await?); + devices.append(&mut Self::init_hid_devices(connection, device).await?); } Ok(devices) @@ -430,13 +413,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 @@ -450,39 +430,44 @@ 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; } } if do_slash { - if let Ok(dev_type) = DeviceHandle::new_slash_usb().await { - if let DeviceHandle::Slash(slash) = dev_type.clone() { - let path = dbus_path_for_slash(); - let ctrl = SlashZbus::new(slash); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start Slash tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: None, - }); - } - } + let slash = if let Ok(dev_type) = DeviceHandle::maybe_slash().await { + Some(dev_type) + } else if let Ok(dev_type) = DeviceHandle::new_slash_usb().await { + Some(dev_type) } else { info!("Tested device was not Slash"); + None + }; + if let Some(dev_type) = slash + && let DeviceHandle::Slash(slash) = dev_type.clone() + { + let path = dbus_path_for_slash(); + let ctrl = SlashZbus::new(slash); + if ctrl + .start_tasks(connection, path.clone()) + .await + .map_err(|e| { + error!("Failed to start Slash tasks: {e:?}, not adding this device") + }) + .is_ok() + { + devices.push(AsusDevice { + device: dev_type, + dbus_path: path, + hid_key: None, + }); + } } } if do_anime { - if let Ok(dev_type) = DeviceHandle::maybe_anime_usb().await { - // TODO: this is copy/pasted + if let Ok(dev_type) = DeviceHandle::maybe_anime().await { if let DeviceHandle::AniMe(anime) = dev_type.clone() { let path = dbus_path_for_anime(); let ctrl = AniMeZbus::new(anime); @@ -505,35 +490,25 @@ impl DeviceManager { } if do_kb_backlight { - // TUF AURA LAPTOP DEVICE - // product_name = ASUS TUF Gaming F15 FX507ZE_FX507ZE - // product_family = ASUS TUF Gaming F15 - let product_name = DMIID::new().unwrap_or_default().product_name; - let product_family = DMIID::new().unwrap_or_default().product_family; - info!( - "No USB keyboard aura, system is {product_name}, try using sysfs backlight control" - ); - if product_name.contains("TUF") || product_family.contains("TUF") { - info!("TUF laptop, try using sysfs backlight control"); - if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(None, "tuf").await - && let DeviceHandle::Aura(aura) = dev_type.clone() + info!("No USB keyboard aura, try sysfs backlight / Dynamic Lighting"); + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(None, "tuf").await + && let DeviceHandle::Aura(aura) = dev_type.clone() + { + let path = dbus_path_for_tuf(); + let ctrl = AuraZbus::new(aura); + if ctrl + .start_tasks(connection, path.clone()) + .await + .map_err(|e| { + error!("Failed to start TUF Aura tasks: {e:?}, not adding this device") + }) + .is_ok() { - let path = dbus_path_for_tuf(); - let ctrl = AuraZbus::new(aura); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start TUF Aura tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: None, - }); - } + devices.push(AsusDevice { + device: dev_type, + dbus_path: path, + hid_key: None, + }); } } } @@ -547,19 +522,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()?; @@ -588,7 +560,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" { @@ -706,11 +677,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) = @@ -733,10 +699,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..03728d1d2 100644 --- a/asusd/src/aura_slash/mod.rs +++ b/asusd/src/aura_slash/mod.rs @@ -2,6 +2,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,6 +14,8 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Slash { + /// Kernel `asus::slash` LED when Dynamic Lighting owns the device. + led: Option, hid: Option>>, usb: Option>>, config: Arc>, @@ -20,11 +23,25 @@ pub struct Slash { impl Slash { pub fn new( + led: Option, hid: Option>>, usb: Option>>, config: Arc>, ) -> Self { - Self { hid, usb, config } + Self { + led, + hid, + usb, + config, + } + } + + pub fn has_sysfs_led(&self) -> bool { + self.led.is_some() + } + + pub fn led(&self) -> Option<&SlashLed> { + self.led.as_ref() } pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { @@ -32,10 +49,17 @@ impl Slash { } pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { + if self.led.is_some() { + return Err(RogError::MissingFunction( + "Slash Dynamic Lighting owns the device; USB/HID packets are not used".into(), + )); + } if let Some(hid) = &self.hid { hid.lock().await.write_bytes(message)?; } else if let Some(usb) = &self.usb { usb.lock().await.write_bytes(message)?; + } else { + return Err(RogError::MissingFunction("No Slash backend".into())); } Ok(()) } @@ -43,15 +67,20 @@ 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; + if let Some(led) = &self.led { + let brightness = if config.enabled { config.brightness } else { 0 }; + led.set_brightness(brightness)?; + led.set_interval(config.display_interval)?; + led.set_mode(&config.display_mode.to_string())?; + return Ok(()); + } + for pkt in &slash_pkt_init(config.slash_type) { self.write_bytes(pkt).await?; } self.write_bytes(&slash_pkt_enable(config.slash_type, config.enabled)) .await?; - - // Apply config upon initialization let option_packets = slash_pkt_options( config.slash_type, config.enabled, @@ -59,11 +88,8 @@ impl Slash { config.display_interval, ); self.write_bytes(&option_packets).await?; - let mode_packets = slash_pkt_set_mode(config.slash_type, config.display_mode); - // self.node.write_bytes(&mode_packets[0])?; self.write_bytes(&mode_packets[1]).await?; - Ok(()) } } diff --git a/asusd/src/aura_slash/trait_impls.rs b/asusd/src/aura_slash/trait_impls.rs index 70aa1ad36..c933aa673 100644 --- a/asusd/src/aura_slash/trait_impls.rs +++ b/asusd/src/aura_slash/trait_impls.rs @@ -13,6 +13,37 @@ use super::Slash; use crate::Reloadable; use crate::error::RogError; +const SLASH_POWER_UNSUPPORTED: &str = concat!( + "Kernel Slash LED has no power_states; ", + "boot/sleep/shutdown/battery/lid persistence is not supported" +); + +fn slash_power_not_supported() -> zbus::fdo::Error { + zbus::fdo::Error::NotSupported(SLASH_POWER_UNSUPPORTED.into()) +} + +fn slash_sysfs_power_supported(slash: &Slash) -> bool { + slash.led().is_some_and(|led| led.has_power_states()) +} + +/// Update one DL `power_states` token (`boot` / `awake` / `sleep` / `shutdown`). +fn slash_set_dl_power_token(slash: &Slash, token: &str, enable: bool) -> Result<(), RogError> { + let led = slash + .led() + .ok_or_else(|| RogError::MissingFunction("Slash LED missing for power_states".into()))?; + let raw = led.get_power_states().map_err(RogError::from)?; + let owned: Vec = raw.split_whitespace().map(str::to_owned).collect(); + let mut states: Vec<&str> = owned.iter().map(String::as_str).collect(); + let present = states.contains(&token); + if enable && !present { + states.push(token); + } else if !enable { + states.retain(|s| *s != token); + } + led.set_power_states_list(&states).map_err(RogError::from)?; + Ok(()) +} + #[derive(Clone)] pub struct SlashZbus(Slash); @@ -26,7 +57,6 @@ impl SlashZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -34,22 +64,29 @@ impl SlashZbus { .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } #[interface(name = "xyz.ljones.Slash")] impl SlashZbus { - /// Get enabled or not + /// True when boot/sleep/... persistence toggles are backed by HID/USB + /// or by Slash Dynamic Lighting `power_states`. + #[zbus(property)] + async fn power_triggers_supported(&self) -> bool { + !self.0.has_sysfs_led() || slash_sysfs_power_supported(&self.0) + } + #[zbus(property)] async fn enabled(&self) -> bool { let lock = self.0.lock_config().await; lock.enabled } - /// Set enabled true or false #[zbus(property)] async fn set_enabled(&self, enabled: bool) { let mut config = self.0.lock_config().await; @@ -58,55 +95,62 @@ 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; config.write(); } - /// Get brightness level #[zbus(property)] async fn brightness(&self) -> u8 { let config = self.0.lock_config().await; config.brightness } - /// Set brightness level #[zbus(property)] 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; @@ -119,19 +163,23 @@ impl SlashZbus { config.display_interval } - /// Set interval between slash animations (0-255) #[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_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(); @@ -143,28 +191,29 @@ impl SlashZbus { Ok(config.display_mode as u8) } - /// Set interval between slash animations (0-255) #[zbus(property)] async fn set_mode(&self, mode: u8) -> zbus::Result<()> { - let mode = SlashMode::try_from(mode).map_err(|err| { - zbus::fdo::Error::InvalidArgs(format!("ctrl_slash::set_mode {}", err)) - })?; + let mode = SlashMode::try_from(mode) + .map_err(|err| zbus::fdo::Error::InvalidArgs(format!("ctrl_slash::set_mode {err}")))?; 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_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.0.write_bytes(&command_packets[1]).await?; + self.0 + .write_bytes(&slash_pkt_save(config.slash_type)) + .await?; + } config.display_mode = mode; config.write(); Ok(()) } - /// 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) @@ -178,6 +227,16 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_boot(&self, enable: bool) -> zbus::Result<()> { + if slash_sysfs_power_supported(&self.0) { + slash_set_dl_power_token(&self.0, "boot", enable)?; + let mut config = self.0.lock_config().await; + config.show_on_boot = enable; + config.write(); + return Ok(()); + } + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } let mut config = self.0.lock_config().await; self.0 .write_bytes(&slash_pkt_boot(config.slash_type, enable)) @@ -195,6 +254,16 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_sleep(&self, enable: bool) -> zbus::Result<()> { + if slash_sysfs_power_supported(&self.0) { + slash_set_dl_power_token(&self.0, "sleep", enable)?; + let mut config = self.0.lock_config().await; + config.show_on_sleep = enable; + config.write(); + return Ok(()); + } + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } let mut config = self.0.lock_config().await; self.0 .write_bytes(&slash_pkt_sleep(config.slash_type, enable)) @@ -212,6 +281,16 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_shutdown(&self, enable: bool) -> zbus::Result<()> { + if slash_sysfs_power_supported(&self.0) { + slash_set_dl_power_token(&self.0, "shutdown", enable)?; + let mut config = self.0.lock_config().await; + config.show_on_shutdown = enable; + config.write(); + return Ok(()); + } + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } let mut config = self.0.lock_config().await; self.0 .write_bytes(&slash_pkt_shutdown(config.slash_type, enable)) @@ -229,6 +308,9 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_battery(&self, enable: bool) -> zbus::Result<()> { + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } let mut config = self.0.lock_config().await; self.0 .write_bytes(&slash_pkt_battery_saver(config.slash_type, enable)) @@ -246,6 +328,9 @@ impl SlashZbus { #[zbus(property)] async fn set_show_battery_warning(&self, enable: bool) -> zbus::Result<()> { + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } let mut config = self.0.lock_config().await; self.0 .write_bytes(&slash_pkt_low_battery(config.slash_type, enable)) @@ -263,13 +348,13 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_lid_closed(&self, enable: bool) -> zbus::Result<()> { + if self.0.has_sysfs_led() { + return Err(slash_power_not_supported().into()); + } 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(()) @@ -279,42 +364,6 @@ impl SlashZbus { impl Reloadable for SlashZbus { async fn reload(&mut self) -> Result<(), RogError> { debug!("reloading slash settings"); - let config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("set_options {}", err); - }) - .ok(); - - macro_rules! write_bytes_with_warning { - ($packet_fn:expr, $cfg:ident, $warn_msg:expr) => { - self.0 - .write_bytes(&$packet_fn(config.slash_type, config.$cfg)) - .await - .map_err(|err| { - warn!("{} {}", $warn_msg, err); - }) - .ok(); - }; - } - - write_bytes_with_warning!(slash_pkt_boot, show_on_boot, "show_on_boot"); - write_bytes_with_warning!(slash_pkt_sleep, show_on_sleep, "show_on_sleep"); - write_bytes_with_warning!(slash_pkt_shutdown, show_on_shutdown, "show_on_shutdown"); - write_bytes_with_warning!(slash_pkt_battery_saver, show_on_battery, "show_on_battery"); - write_bytes_with_warning!( - slash_pkt_low_battery, - show_battery_warning, - "show_battery_warning" - ); - - Ok(()) + self.0.do_initialization().await } } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index c31de89a0..24f16b8de 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -1,11 +1,14 @@ use std::sync::Arc; use config_traits::{StdConfig, StdConfigLoad}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use rog_anime::AnimeType; use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; use rog_aura::AuraDeviceType; +use rog_platform::DynamicLed; +use rog_platform::SlashLed; +use rog_platform::XboxLogoLed; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; @@ -27,7 +30,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, @@ -41,17 +43,30 @@ 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. + /// Prefer `asus::slash` Dynamic Lighting when the kernel exposes it. + pub async fn maybe_slash() -> Result { + debug!("Testing for Slash sysfs LED"); + 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(); + // Packet layout unused under sysfs; keep DMI type for UI/logging. + config.slash_type = SlashType::from_dmi(); + let slash = Slash::new(Some(led), None, None, Arc::new(Mutex::new(config))); + slash.do_initialization().await?; + Ok(Self::Slash(slash)) + } + + /// Legacy Slash HID when no `asus::slash` node is present. pub async fn new_slash_hid( device: Arc>, prod_id: &str, @@ -72,12 +87,12 @@ impl DeviceHandle { let mut config = SlashConfig::new().load(); config.slash_type = slash_type; - let slash = Slash::new(Some(device), None, Arc::new(Mutex::new(config))); + let slash = Slash::new(None, Some(device), None, Arc::new(Mutex::new(config))); slash.do_initialization().await?; Ok(Self::Slash(slash)) } - /// Try Slash USB. If one exists it is initialsed and returned. + /// Legacy Slash USB when no `asus::slash` node is present. pub async fn new_slash_usb() -> Result { debug!("Testing for USB Slash"); let slash_type = SlashType::from_dmi(); @@ -87,10 +102,10 @@ impl DeviceHandle { 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, None, Some(Arc::new(Mutex::new(usb))), Arc::new(Mutex::new(config)), @@ -98,39 +113,37 @@ impl DeviceHandle { slash.do_initialization().await?; Ok(Self::Slash(slash)) } else { - Err(RogError::NotFound("No slash device found".to_string())) + Err(RogError::Slash(SlashError::NoDevice)) } } - /// Try AniMe Matrix HID. If one exists it is initialsed and returned. - pub async fn maybe_anime_hid( - _device: Arc>, - _prod_id: &str, - ) -> Result { - // TODO: can't use HIDRAW for anime at the moment - Err(RogError::NotFound( - "Can't use anime over hidraw yet. Skip.".to_string(), - )) - - // debug!("Testing for HIDRAW AniMe"); - // let anime_type = AnimeType::from_dmi(); - // dbg!(prod_id); - // if matches!(anime_type, AnimeType::Unsupported) || prod_id != "193b" - // { log::info!("Unknown or invalid AniMe: {prod_id:?}, - // skipping"); return Err(RogError::NotFound("No - // anime-matrix device".to_string())); } - // info!("Found AniMe Matrix HIDRAW {anime_type:?}: {prod_id}"); - - // let mut config = AniMeConfig::new().load(); - // config.anime_type = anime_type; - // let mut anime = AniMe::new(Some(device), None, - // Arc::new(Mutex::new(config))); anime.do_initialization(). - // await?; Ok(Self::AniMe(anime)) - } + /// Prefer `asus::anime` Dynamic Lighting; fall back to USB only when the + /// kernel node is absent (older kernels without AniMe DL). + pub async fn maybe_anime() -> Result { + debug!("Testing for AniMe Matrix"); + let mut anime_type = get_anime_type(); + + if let Ok(led) = DynamicLed::find("asus::anime") { + if matches!(anime_type, AnimeType::Unsupported) { + // Kernel uses a GA402-like layout for unknown AniMe boards. + anime_type = AnimeType::GA402; + info!("AniMe DL on unknown board; using GA402 layout"); + } + info!( + "Found AniMe Matrix Dynamic Lighting ({anime_type:?}, {})", + led.name() + ); + let mut config = AniMeConfig::new().load(); + config.anime_type = anime_type; + let mut anime = AniMe::new( + None, + Some(Arc::new(Mutex::new(led))), + Arc::new(Mutex::new(config)), + ); + anime.do_initialization().await?; + return Ok(Self::AniMe(anime)); + } - pub async fn maybe_anime_usb() -> Result { - debug!("Testing for USB AniMe"); - let anime_type = get_anime_type(); if matches!(anime_type, AnimeType::Unsupported) { info!("No Anime Matrix capable laptop found"); return Err(RogError::Anime(AnimeError::NoDevice)); @@ -142,8 +155,8 @@ 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))), + None, Arc::new(Mutex::new(config)), ); anime.do_initialization().await?; @@ -190,19 +203,96 @@ impl DeviceHandle { } info!("Found laptop aura type {prod_id:?}"); - let backlight = KeyboardBacklight::new() - .map_err(|e| error!("Keyboard backlight error: {e:?}")) - .map_or(None, |k| { - info!("Found sysfs backlight control"); - Some(Arc::new(Mutex::new(k))) + // Prefer asus::keyboard; on TUF, asus-wmi may register the same DL ABI + // on asus::kbd_backlight when ASUS_WMI_DEVID_TUF_RGB_MODE* is present. + let (dynamic_kbd, dynamic_lightbar, dynamic_logo, dl_owns_kbd_backlight) = { + let kbd = DynamicLed::find("asus::keyboard") + .or_else(|_| DynamicLed::find("asus::kbd_backlight")) + .ok(); + let dl_owns_kbd_backlight = kbd + .as_ref() + .is_some_and(|k| k.name().contains("kbd_backlight")); + let kbd = kbd.map(|k| { + info!("Dynamic Lighting keyboard detected: {}", k.name()); + Arc::new(Mutex::new(k)) }); + let lb = DynamicLed::find("asus::lightbar") + .map(|l| { + info!("Dynamic Lighting lightbar detected: {}", l.name()); + Arc::new(Mutex::new(l)) + }) + .ok(); + let logo = DynamicLed::find("asus::logo") + .map(|l| { + info!("Dynamic Lighting logo detected: {}", l.name()); + Arc::new(Mutex::new(l)) + }) + .ok(); + if kbd.is_some() || lb.is_some() || logo.is_some() { + (kbd, lb, logo, dl_owns_kbd_backlight) + } else { + debug!( + "Dynamic Lighting not detected; using legacy hidraw / TUF platform fallback" + ); + (None, None, None, false) + } + }; + + // When any keyboard DL node is present it owns brightness. Do not also + // keep a WMI/ally KeyboardBacklight handle (dual owner on G614-class). + let backlight = if dynamic_kbd.is_some() { + if dl_owns_kbd_backlight { + debug!("Skipping legacy KeyboardBacklight; Dynamic Lighting owns kbd_backlight"); + } else { + debug!( + "Skipping legacy KeyboardBacklight; Dynamic Lighting owns keyboard brightness" + ); + } + None + } else { + KeyboardBacklight::new() + .map_err(|e| error!("Keyboard backlight error: {e:?}")) + .map_or(None, |k| { + info!("Found sysfs backlight control"); + Some(Arc::new(Mutex::new(k))) + }) + }; + + // One owner: when DL validates, drop hidraw so two paths never fight. + let hid = if dynamic_kbd.is_some() || dynamic_lightbar.is_some() || dynamic_logo.is_some() { + None + } else { + device + }; + + let xbox_logo = XboxLogoLed::new() + .map(|led| { + info!("Found Xbox Guide LED at {:?}", led.path()); + Arc::new(Mutex::new(led)) + }) + .ok(); // 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; + if dynamic_kbd.is_none() + && dynamic_lightbar.is_none() + && dynamic_logo.is_none() + && hid.is_none() + && backlight.is_none() + { + return Err(RogError::NotFound( + "No usable Aura lighting backend".to_string(), + )); + } + let aura = Aura { - hid: device, + dynamic_kbd, + dynamic_lightbar, + dynamic_logo, + hid, backlight, + xbox_logo, config: Arc::new(Mutex::new(config)), }; aura.do_initialization().await?; diff --git a/rog-dbus/src/zbus_slash.rs b/rog-dbus/src/zbus_slash.rs index d5eb11eed..f39a36532 100644 --- a/rog-dbus/src/zbus_slash.rs +++ b/rog-dbus/src/zbus_slash.rs @@ -30,6 +30,10 @@ pub trait Slash { #[zbus(property)] fn set_mode(&self, value: u8) -> zbus::Result<()>; + /// True when boot/sleep/... persistence toggles are backed by HID/USB. + #[zbus(property)] + fn power_triggers_supported(&self) -> zbus::Result; + /// ShowBatteryWarning property #[zbus(property)] fn show_battery_warning(&self) -> zbus::Result; diff --git a/rog-slash/src/data.rs b/rog-slash/src/data.rs index 8cd0a50e6..f25940298 100644 --- a/rog-slash/src/data.rs +++ b/rog-slash/src/data.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use std::str::FromStr; +#[cfg(feature = "detect")] use dmi_id::DMIID; use serde::{Deserialize, Serialize}; #[cfg(feature = "dbus")] @@ -56,6 +57,18 @@ impl SlashType { } } + /// Select hidraw packet layout from the USB product id. + /// Report 0x5e vs 0x5d is a device capability, not a DMI board name. + pub fn from_usb_product(id: &str) -> Self { + match id.trim_start_matches("0x").to_ascii_uppercase().as_str() { + PROD_ID1_STR => SlashType::GA403_2024, + PROD_ID2_STR => SlashType::GA403_2025, + _ => SlashType::Unsupported, + } + } + + /// Board-name detection used by legacy HID/USB Slash control. + #[cfg(feature = "detect")] pub fn from_dmi() -> Self { let board_name = DMIID::new().unwrap_or_default().board_name.to_uppercase(); if board_name.contains("G614F") { From 33973f3ac65e80efd0ba6424549406111fe018c0 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 11:53:44 +0100 Subject: [PATCH 4/6] docs: document Dynamic Lighting userspace ABI Describe asus::* topology, ownership versus hidraw, and how asusd maps D-Bus Aura/Slash/AniMe onto kernel Dynamic Lighting. Signed-off-by: Marco Scardovi --- docs/SUMMARY.md | 1 + docs/usage/dynamic-lighting.md | 100 +++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 docs/usage/dynamic-lighting.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4340c9385..279225281 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -24,6 +24,7 @@ # Usage - [asusctl](usage/asusctl.md) +- [Dynamic Lighting and legacy fallback](usage/dynamic-lighting.md) # FAQ diff --git a/docs/usage/dynamic-lighting.md b/docs/usage/dynamic-lighting.md new file mode 100644 index 000000000..53b5da77a --- /dev/null +++ b/docs/usage/dynamic-lighting.md @@ -0,0 +1,100 @@ +# Dynamic Lighting and legacy fallback + +asusd prefers the Linux LED Dynamic Lighting ABI when a device exposes a valid +node below `/sys/class/leds`. A valid node must provide the generic `effect`, +`effect_index`, `zone_type`, and `led_count` attributes. Optional speed, +direction, palette, power-state, enable, and direct-buffer controls are used +only when the node advertises them. + +On supported ROG laptop keyboards, asusd retains the matching `hidraw` output +report as a capability-based fallback when no valid Dynamic Lighting node is +available. The two paths are mutually exclusive for a physical device: +Dynamic Lighting always wins, and hidraw is not used to retry an effect rejected +by the kernel. This preserves operation on released kernels while avoiding two +owners sending commands to the same controller. + +## Topology + +hid-asus registers independent chassis zones: + +| Node | Role | +|------|------| +| `asus::keyboard` | Keyboard; firmware dimmer | +| `asus::lightbar` | Chassis lightbar when present (`enabled` gates the zone; dimmer only if kernel advertises brightness) | +| `asus::logo` | Chassis logo zone when the kernel registers a separate DL node | +| `asus::slash` | Slash LED (DL `effect`/`speed`/`power_states` when present; else legacy `slash_mode` / `slash_interval`) | +| `asus::anime` | AniMe Matrix (`direct_buffer` stride from `direct_sample_size`, typically 1) | +| `asus::xbox_logo` | Xbox Guide LED on Xbox Ally X (brightness 0–3; DL effects when advertised) | +| `asus::kbd_backlight` | TUF WMI keyboard when asus-wmi exposes the DL ABI | + +asusd prefers `asus::keyboard`, then falls back to a Dynamic Lighting-capable +`asus::kbd_backlight`. Brightness-only `asus::kbd_backlight` (no +`effect`/`effect_index`/`zone_type`/`led_count`) is not Dynamic Lighting: it +remains the legacy TUF/`KeyboardBacklight` path only when no keyboard DL node +is present. When any keyboard DL node exists, the legacy platform brightness +handle is not kept. + +There is no `asus::global` aggregate and no userspace `aura_mode` selector. +Each node accepts its own effect, palette, and direct writes. Whole-device +requests fan out to every present zone (keyboard, lightbar, logo). Intensity +(`brightness`) is written primarily to the keyboard node; lightbar uses +`enabled` (and brightness only when the kernel exposes a dimmer). + +`direct_sample_size` (when present) is the authoritative packed stride for +`direct_buffer`. Older kernels omit it; asusd then falls back to mono for +AniMe/`zone_type=matrix` and RGB elsewhere. + +Lighting off is `enabled=false`, independent from `effect`. `brightness` stays +intensity (scaled from the D-Bus `LedBrightness` levels when +`max_brightness > 3`). Firmware effects, including static colour, are committed +with Aura `0xb3`/`0xb5`/`0xb4`; `enabled` gates the zone with `0xbd`. + +Kernel direct RGB may use HID LampArray (Usage Page `0x59`) when Aura `0xBC` +cannot drive the lightbar independently; firmware animations stay on Aura +`0xb3`. That backend choice is invisible to the sysfs ABI. + +## AniMe Matrix + +When `asus::anime` is present, asusd drives AniMe exclusively through Dynamic +Lighting (USB `0x193b` packets are not used). Builtin stage names are the +kernel effect strings (`boot_*`, `awake_*`, `sleep_*`, `shutdown_*`). Direct +frames use `led_count` intensity bytes. USBRaw remains a fallback only on +kernels without the AniMe DL node. + +## Slash + +Slash prefers `asus::slash` when present. When that node is full Dynamic +Lighting with `power_states`, D-Bus boot/sleep/shutdown map to those tokens. +Battery/lid persistence remains HID/USB-only (or `NotSupported` under sysfs +without matching kernel attrs). Legacy kernels keep `slash_mode` / +`slash_interval`. Without any Slash LED node, asusd falls back to HID/USB. + +## Ally / Xbox Ally + +Ally effect lists come from `effect_index` (narrower than notebooks). Xbox Ally +X may advertise `reactive`. LED power uses `power_states` under DL; the legacy +hidraw Ally power fix is not used when Dynamic Lighting owns the device. + +## D-Bus zones + +The D-Bus API still describes historical keyboard and left/right lightbar +subzones. Under Dynamic Lighting those are not advertised +(`supported_basic_zones` is empty). Incoming legacy zone values are accepted +only as aliases: Key1–4 map to the keyboard node, BarLeft/BarRight to the +lightbar, Logo to `asus::logo` when present, and `None` fans out to every +present zone. + +## Other devices + +ROG NVMe enclosure lighting requires the kernel ASUS Aura SCSI Dynamic Lighting +driver. asusd matches an enclosure's block-device ancestry to its exact LED +node and briefly retries while that node is being registered. It never falls +back to the first enclosure. The public `rog_scsi` helpers (`Device`, `Task`, +`sg`, and `Device::perform`) remain available and still issue SG_IO, but for +enclosure lighting the Dynamic Lighting sysfs ABI is the recommended backend: +vendor animation commands are kernel-owned once an enclosure LED node exists. + +The optional generic `frame` attribute is not currently used by asusd. Direct +streaming uses `direct_buffer` with `led_count * sample_size` bytes (RGB = 3, +AniMe mono = 1). Hardware-specific behavior still depends on the kernel driver +reporting correct topology, LED count, and optional attributes. From 6b6b50ea882ea7aeb450c7931cb94df3699addb9 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 11:53:44 +0100 Subject: [PATCH 5/6] feat(rog-control-center): list device-reported Aura modes Surface every mode advertised by the active Dynamic Lighting node and refresh Slash UI strings for the sysfs-backed path. Signed-off-by: Marco Scardovi --- rog-control-center/src/ui/setup_aura.rs | 3 +- rog-control-center/src/ui/setup_slash.rs | 12 + .../translations/en/rog-control-center.po | 676 ++++++++++-------- rog-control-center/ui/pages/slash.slint | 17 +- rog-control-center/ui/types/aura_types.slint | 23 +- 5 files changed, 401 insertions(+), 330 deletions(-) diff --git a/rog-control-center/src/ui/setup_aura.rs b/rog-control-center/src/ui/setup_aura.rs index 38ffa2d14..22b1d17fa 100644 --- a/rog-control-center/src/ui/setup_aura.rs +++ b/rog-control-center/src/ui/setup_aura.rs @@ -105,7 +105,8 @@ pub fn setup_aura_page( let mut mode_names = Vec::new(); for (i, name) in names.iter().enumerate() { let raw = i as i32; - if modes_vec.contains(&raw) && i != 9 { + // Skip Direct (13): the basic dropdown has no frame producer. + if modes_vec.contains(&raw) && i != 9 && raw != 13 { raws.push(raw); mode_names.push(name.clone()); } diff --git a/rog-control-center/src/ui/setup_slash.rs b/rog-control-center/src/ui/setup_slash.rs index a10698fba..69db1cf0d 100644 --- a/rog-control-center/src/ui/setup_slash.rs +++ b/rog-control-center/src/ui/setup_slash.rs @@ -47,6 +47,18 @@ pub fn setup_slash_page(ui: &MainWindow, _states: Arc>) { set_ui_props_async!(handle, slash, SlashPageData, enabled); set_ui_props_async!(handle, slash, SlashPageData, brightness); set_ui_props_async!(handle, slash, SlashPageData, interval); + match slash.power_triggers_supported().await { + Ok(supported) => { + let handle2 = handle.clone(); + handle2 + .upgrade_in_event_loop(move |ui| { + ui.global::() + .set_power_triggers_supported(supported); + }) + .ok(); + } + Err(e) => error!("setup_slash: power_triggers_supported: {e}"), + } set_ui_props_async!(handle, slash, SlashPageData, show_on_boot); set_ui_props_async!(handle, slash, SlashPageData, show_on_shutdown); set_ui_props_async!(handle, slash, SlashPageData, show_on_sleep); diff --git a/rog-control-center/translations/en/rog-control-center.po b/rog-control-center/translations/en/rog-control-center.po index f6f769a56..f550c7090 100644 --- a/rog-control-center/translations/en/rog-control-center.po +++ b/rog-control-center/translations/en/rog-control-center.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-07-23 18:09+0000\n" +"POT-Creation-Date: 2026-11-12 10:53+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -12,6 +12,78 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +#: rog-control-center/ui/pages/about.slint:27 +msgctxt "PageAbout" +msgid "" +"\nA powerful graphical interface for managing ASUS ROG, TUF, and ProArt " +"laptops on Linux. It acts as the official GUI for the asusctl toolset, " +"allowing for seamless hardware tuning without the command line." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:32 +msgctxt "PageAbout" +msgid "Key Features:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:36 +msgctxt "PageAbout" +msgid "" +" • Performance: Switch power profiles and customize fan curves." +"\n • Aura Sync: Control keyboard backlighting and LED effects.\n " +"• Battery Health: Set charge limits to extend battery longevity.\n • " +"Display: Toggle Panel Overdrive and refresh rates.\n • AniMe Matrix: " +"Control AniMe Matrix displays." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:41 +msgctxt "PageAbout" +msgid "Requirements:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:46 +msgctxt "PageAbout" +msgid "" +" This software requires kernel version 6.19 or later. However due to " +"active development, the latest kernel is always the recommended one." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:51 +msgctxt "PageAbout" +msgid "Work in progress:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:55 +msgctxt "PageAbout" +msgid "" +" • Theme the widgets\n • Add a cpu/gpu temp/fan speed info " +"bar\n • Include fan speeds, temps in a bottom bar\n • Screenpad " +"controls\n • ROG Ally specific settings" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:60 +msgctxt "PageAbout" +msgid "License:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:64 +msgctxt "PageAbout" +msgid "" +" This project is open-source software licensed under the Mozilla Public " +"License 2.0 (MPL-2.0)." +msgstr "" + +#: rog-control-center/ui/pages/about.slint:69 +msgctxt "PageAbout" +msgid "Links:" +msgstr "" + +#: rog-control-center/ui/pages/about.slint:73 +msgctxt "PageAbout" +msgid "" +" Source Code: https://github.com/OpenGamingCollective/asusctl\n " +"Website: https://asus-linux.org/" +msgstr "" + #: rog-control-center/ui/pages/anime.slint:6 msgctxt "Anime Brightness" msgid "Off" @@ -138,6 +210,71 @@ msgctxt "PageAnime" msgid "Off when on battery" msgstr "" +#: rog-control-center/ui/pages/app_settings.slint:45 +msgctxt "PageAppSettings" +msgid "Run in background after closing" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:53 +msgctxt "PageAppSettings" +msgid "Start app in background (UI closed)" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:61 +msgctxt "PageAppSettings" +msgid "Enable system tray icon" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:69 +msgctxt "PageAppSettings" +msgid "Enable dGPU notifications" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:77 +msgctxt "PageAppSettings" +msgid "Start app on system startup" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:87 +msgctxt "PageAppSettings" +msgid "Start in background on startup" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:97 +msgctxt "PageAppSettings" +msgid "Armory/ROG key to show/hide window (keep app running in background)" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:113 +msgctxt "PageAppSettings" +msgid "Setting up…" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:115 +msgctxt "PageAppSettings" +msgid "Shortcut active" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:117 +msgctxt "PageAppSettings" +msgid "No shortcut assigned" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:119 +msgctxt "PageAppSettings" +msgid "Global shortcuts unavailable" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:132 +msgctxt "PageAppSettings" +msgid "Set up shortcut" +msgstr "" + +#: rog-control-center/ui/pages/app_settings.slint:133 +msgctxt "PageAppSettings" +msgid "Configure shortcut" +msgstr "" + #: rog-control-center/ui/pages/aura.slint:29 msgctxt "PageAura" msgid "Brightness" @@ -244,208 +381,65 @@ msgctxt "PageFans" msgid "Quiet" msgstr "" -#: rog-control-center/ui/pages/slash.slint:51 -msgctxt "PageSlash" -msgid "Slash lighting" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:56 -msgctxt "PageSlash" -msgid "Enable A-cover lighting" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:64 -msgctxt "PageSlash" -msgid "Brightness" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:77 -msgctxt "PageSlash" -msgid "Animation speed" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:90 -msgctxt "PageSlash" -msgid "Animation" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:110 -msgctxt "PageSlash" -msgid "Visibility triggers" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:115 -msgctxt "PageSlash" -msgid "Show on boot" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:123 -msgctxt "PageSlash" -msgid "Show on shutdown" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:131 -msgctxt "PageSlash" -msgid "Show on sleep" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:139 -msgctxt "PageSlash" -msgid "Show on battery" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:147 -msgctxt "PageSlash" -msgid "Show low-battery warning" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:155 -msgctxt "PageSlash" -msgid "Show when lid is closed" -msgstr "" - -#: rog-control-center/ui/pages/slash.slint:170 -msgctxt "Slash_tip" -msgid "" -"Slash Lighting controls the A-cover LED strip found on supported Zephyrus " -"models." -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:24 -msgctxt "PageBattery" -msgid "Battery Information" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:29 -msgctxt "PageBattery" -msgid "Charge limit" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:54 -msgctxt "PageBattery" -msgid "Battery Health (Capacity):" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:67 -msgctxt "PageBattery" -msgid "Status:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:80 -msgctxt "PageBattery" -msgid "Power Draw:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:93 -msgctxt "PageBattery" -msgid "Time to Full:" -msgstr "" - -#: rog-control-center/ui/pages/battery.slint:93 -msgctxt "PageBattery" -msgid "Time Remaining:" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:27 -msgctxt "PageAppSettings" -msgid "Run in background after closing" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:35 -msgctxt "PageAppSettings" -msgid "Start app in background (UI closed)" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:43 -msgctxt "PageAppSettings" -msgid "Enable system tray icon" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:51 -msgctxt "PageAppSettings" -msgid "Enable dGPU notifications" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:59 -msgctxt "PageAppSettings" -msgid "Start app on system startup" -msgstr "" - -#: rog-control-center/ui/pages/app_settings.slint:69 -msgctxt "PageAppSettings" -msgid "Start in background on startup" +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Integrated" msgstr "" -#: rog-control-center/ui/pages/about.slint:27 -msgctxt "PageAbout" -msgid "" -"\nA powerful graphical interface for managing ASUS ROG, TUF, and ProArt " -"laptops on Linux. It acts as the official GUI for the asusctl toolset, " -"allowing for seamless hardware tuning without the command line." +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Hybrid" msgstr "" -#: rog-control-center/ui/pages/about.slint:32 -msgctxt "PageAbout" -msgid "Key Features:" +#: rog-control-center/ui/pages/gpu.slint:29 +#: rog-control-center/ui/pages/gpu.slint:29 +msgctxt "PageGPU" +msgid "Ultimate" msgstr "" -#: rog-control-center/ui/pages/about.slint:36 -msgctxt "PageAbout" -msgid "" -" • Performance: Switch power profiles and customize fan curves." -"\n • Aura Sync: Control keyboard backlighting and LED effects.\n " -"• Battery Health: Set charge limits to extend battery longevity.\n • " -"Display: Toggle Panel Overdrive and refresh rates.\n • AniMe Matrix: " -"Control AniMe Matrix displays." +#: rog-control-center/ui/pages/gpu.slint:48 +msgctxt "PageGPU" +msgid "Internal/Discrete GPU" msgstr "" -#: rog-control-center/ui/pages/about.slint:41 -msgctxt "PageAbout" -msgid "Requirements:" +#: rog-control-center/ui/pages/gpu.slint:57 +msgctxt "PageGPU" +msgid "This device has no switchable GPU." msgstr "" -#: rog-control-center/ui/pages/about.slint:46 -msgctxt "PageAbout" +#: rog-control-center/ui/pages/gpu.slint:69 +msgctxt "PageGPU" msgid "" -" This software requires kernel version 6.19 or later. However due to " -"active development, the latest kernel is always the recommended one." -msgstr "" - -#: rog-control-center/ui/pages/about.slint:51 -msgctxt "PageAbout" -msgid "Work in progress:" +"Note: Changes take effect after a reboot. The dropdown always shows the " +"current mode." msgstr "" -#: rog-control-center/ui/pages/about.slint:55 -msgctxt "PageAbout" -msgid "" -" • Theme the widgets\n • Add a cpu/gpu temp/fan speed info " -"bar\n • Include fan speeds, temps in a bottom bar\n • Screenpad " -"controls\n • ROG Ally specific settings" +#: rog-control-center/ui/pages/gpu.slint:76 +msgctxt "PageGPU" +msgid "GPU mode" msgstr "" -#: rog-control-center/ui/pages/about.slint:60 -msgctxt "PageAbout" -msgid "License:" +#: rog-control-center/ui/pages/gpu.slint:97 +msgctxt "PageGPU" +msgid "Note: Reserved GPU memory changes take effect after a reboot." msgstr "" -#: rog-control-center/ui/pages/about.slint:64 -msgctxt "PageAbout" -msgid "" -" This project is open-source software licensed under the Mozilla Public " -"License 2.0 (MPL-2.0)." +#: rog-control-center/ui/pages/gpu.slint:104 +msgctxt "PageGPU" +msgid "Reserved GPU memory" msgstr "" -#: rog-control-center/ui/pages/about.slint:69 -msgctxt "PageAbout" -msgid "Links:" +#: rog-control-center/ui/pages/gpu.slint:118 +msgctxt "PageGPU" +msgid "XG Mobile LED" msgstr "" -#: rog-control-center/ui/pages/about.slint:73 -msgctxt "PageAbout" -msgid "" -" Source Code: https://github.com/OpenGamingCollective/asusctl\n " -"Website: https://asus-linux.org/" +#: rog-control-center/ui/pages/gpu.slint:131 +msgctxt "PageGPU" +msgid "Disable nvidia-powerd on battery" msgstr "" #: rog-control-center/ui/pages/system.slint:21 @@ -512,399 +506,440 @@ msgctxt "SystemPageData" msgid "Multi-zone Strong" msgstr "" -#: rog-control-center/ui/pages/system.slint:213 +#: rog-control-center/ui/pages/system.slint:214 msgctxt "PageSystem" msgid "Power settings" msgstr "" -#: rog-control-center/ui/pages/system.slint:235 +#: rog-control-center/ui/pages/system.slint:236 msgctxt "PageSystem" msgid "Hardware Monitor" msgstr "" -#: rog-control-center/ui/pages/system.slint:248 +#: rog-control-center/ui/pages/system.slint:249 msgctxt "PageSystem" msgid "CPU Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:257 -#: rog-control-center/ui/pages/system.slint:257 -#: rog-control-center/ui/pages/system.slint:257 +#: rog-control-center/ui/pages/system.slint:258 +#: rog-control-center/ui/pages/system.slint:258 +#: rog-control-center/ui/pages/system.slint:258 msgctxt "PageSystem" msgid "Temperature:" msgstr "" -#: rog-control-center/ui/pages/system.slint:270 -#: rog-control-center/ui/pages/system.slint:270 -#: rog-control-center/ui/pages/system.slint:270 +#: rog-control-center/ui/pages/system.slint:271 +#: rog-control-center/ui/pages/system.slint:271 +#: rog-control-center/ui/pages/system.slint:271 msgctxt "PageSystem" msgid "Usage:" msgstr "" -#: rog-control-center/ui/pages/system.slint:283 +#: rog-control-center/ui/pages/system.slint:284 msgctxt "PageSystem" msgid "Frequency:" msgstr "" -#: rog-control-center/ui/pages/system.slint:296 +#: rog-control-center/ui/pages/system.slint:297 msgctxt "PageSystem" msgid "iGPU Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 -#: rog-control-center/ui/pages/system.slint:309 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 +#: rog-control-center/ui/pages/system.slint:310 msgctxt "PageSystem" msgid "N/A" msgstr "" -#: rog-control-center/ui/pages/system.slint:331 +#: rog-control-center/ui/pages/system.slint:332 msgctxt "PageSystem" msgid "Memory Status" msgstr "" -#: rog-control-center/ui/pages/system.slint:340 +#: rog-control-center/ui/pages/system.slint:341 msgctxt "PageSystem" msgid "RAM Usage:" msgstr "" -#: rog-control-center/ui/pages/system.slint:359 +#: rog-control-center/ui/pages/system.slint:360 msgctxt "PageSystem" msgid "dGPU Status" msgstr "" #: rog-control-center/ui/pages/system.slint:373 -#: rog-control-center/ui/pages/system.slint:386 +#: rog-control-center/ui/pages/system.slint:373 msgctxt "PageSystem" msgid "Suspended" msgstr "" -#: rog-control-center/ui/pages/system.slint:394 +#: rog-control-center/ui/pages/system.slint:395 msgctxt "PageSystem" msgid "Fan Speeds" msgstr "" -#: rog-control-center/ui/pages/system.slint:403 +#: rog-control-center/ui/pages/system.slint:404 msgctxt "PageSystem" msgid "CPU Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:416 +#: rog-control-center/ui/pages/system.slint:417 msgctxt "PageSystem" msgid "GPU Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:429 +#: rog-control-center/ui/pages/system.slint:430 msgctxt "PageSystem" msgid "Mid Fan:" msgstr "" -#: rog-control-center/ui/pages/system.slint:448 +#: rog-control-center/ui/pages/system.slint:449 msgctxt "PageSystem" msgid "Platform Profile" msgstr "" -#: rog-control-center/ui/pages/system.slint:458 +#: rog-control-center/ui/pages/system.slint:459 msgctxt "PageSystem" msgid "Advanced" msgstr "" -#: rog-control-center/ui/pages/system.slint:478 +#: rog-control-center/ui/pages/system.slint:479 msgctxt "PageSystem" msgid "Screenpad brightness" msgstr "" -#: rog-control-center/ui/pages/system.slint:502 +#: rog-control-center/ui/pages/system.slint:503 msgctxt "PageSystem" msgid "Sync with primary" msgstr "" -#: rog-control-center/ui/pages/system.slint:522 +#: rog-control-center/ui/pages/system.slint:523 msgctxt "PageSystem" msgid "Armoury settings" msgstr "" -#: rog-control-center/ui/pages/system.slint:532 +#: rog-control-center/ui/pages/system.slint:533 msgctxt "no_asus_armoury_driver_1" msgid "The asus-armoury driver is not loaded" msgstr "" -#: rog-control-center/ui/pages/system.slint:539 +#: rog-control-center/ui/pages/system.slint:540 msgctxt "no_asus_armoury_driver_2" msgid "For advanced features you will require a kernel with this driver added." msgstr "" -#: rog-control-center/ui/pages/system.slint:551 +#: rog-control-center/ui/pages/system.slint:552 msgctxt "PageSystem" msgid "Panel Overdrive" msgstr "" -#: rog-control-center/ui/pages/system.slint:559 +#: rog-control-center/ui/pages/system.slint:560 msgctxt "PageSystem" msgid "POST boot sound" msgstr "" -#: rog-control-center/ui/pages/system.slint:567 +#: rog-control-center/ui/pages/system.slint:568 msgctxt "PageSystem" msgid "Screen Auto Brightness" msgstr "" -#: rog-control-center/ui/pages/system.slint:575 +#: rog-control-center/ui/pages/system.slint:576 msgctxt "PageSystem" msgid "MCU Powersave" msgstr "" -#: rog-control-center/ui/pages/system.slint:584 +#: rog-control-center/ui/pages/system.slint:585 msgctxt "PageSystem" msgid "MiniLED Mode" msgstr "" -#: rog-control-center/ui/pages/system.slint:602 +#: rog-control-center/ui/pages/system.slint:603 msgctxt "ppt_warning" msgid "The following settings are not applied until the toggle is enabled." msgstr "" -#: rog-control-center/ui/pages/system.slint:607 -#: rog-control-center/ui/pages/system.slint:607 +#: rog-control-center/ui/pages/system.slint:608 +#: rog-control-center/ui/pages/system.slint:608 msgctxt "ppt_group_enabled" msgid "Enable Tuning" msgstr "" -#: rog-control-center/ui/pages/system.slint:630 +#: rog-control-center/ui/pages/system.slint:631 msgctxt "requires_fan_curve_warning" msgid "" "CPU power limits require an active custom fan curve. Enable a fan curve for " "the active profile in the Fans tab to unlock these controls." msgstr "" -#: rog-control-center/ui/pages/system.slint:640 -#: rog-control-center/ui/pages/system.slint:640 +#: rog-control-center/ui/pages/system.slint:641 +#: rog-control-center/ui/pages/system.slint:641 msgctxt "ppt_pl1_spl" msgid "CPU Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:642 +#: rog-control-center/ui/pages/system.slint:643 msgctxt "ppt_pl1_spl_help" msgid "" "Long-term CPU power limit that affects sustained workload performance. " "Higher values may increase heat and power consumption." msgstr "" -#: rog-control-center/ui/pages/system.slint:658 -#: rog-control-center/ui/pages/system.slint:658 +#: rog-control-center/ui/pages/system.slint:659 +#: rog-control-center/ui/pages/system.slint:659 msgctxt "ppt_pl2_sppt" msgid "CPU Turbo Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:660 +#: rog-control-center/ui/pages/system.slint:661 msgctxt "ppt_pl2_sppt_help" msgid "" "Short-term CPU power limit for boost periods. Controls maximum power during " "brief high-performance bursts." msgstr "" -#: rog-control-center/ui/pages/system.slint:676 -#: rog-control-center/ui/pages/system.slint:676 +#: rog-control-center/ui/pages/system.slint:677 +#: rog-control-center/ui/pages/system.slint:677 msgctxt "ppt_pl3_fppt" msgid "CPU Fast Burst Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:678 +#: rog-control-center/ui/pages/system.slint:679 msgctxt "ppt_pl3_fppt_help" msgid "" "Ultra-short duration power limit for instantaneous CPU bursts. Affects " "responsiveness during sudden workload spikes." msgstr "" -#: rog-control-center/ui/pages/system.slint:693 -#: rog-control-center/ui/pages/system.slint:693 +#: rog-control-center/ui/pages/system.slint:694 +#: rog-control-center/ui/pages/system.slint:694 msgctxt "ppt_fppt" msgid "Fast Package Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:695 +#: rog-control-center/ui/pages/system.slint:696 msgctxt "ppt_fppt_help" msgid "" "Ultra-short duration power limit for system package. Controls maximum power " "during millisecond-scale load spikes." msgstr "" -#: rog-control-center/ui/pages/system.slint:711 -#: rog-control-center/ui/pages/system.slint:711 +#: rog-control-center/ui/pages/system.slint:712 +#: rog-control-center/ui/pages/system.slint:712 msgctxt "ppt_apu_sppt" msgid "APU Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:713 +#: rog-control-center/ui/pages/system.slint:714 msgctxt "ppt_apu_sppt_help" msgid "" "Long-term power limit for integrated graphics and CPU combined. Affects " "sustained performance of APU-based workloads." msgstr "" -#: rog-control-center/ui/pages/system.slint:729 -#: rog-control-center/ui/pages/system.slint:729 +#: rog-control-center/ui/pages/system.slint:730 +#: rog-control-center/ui/pages/system.slint:730 msgctxt "ppt_platform_sppt" msgid "Platform Sustained Power Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:731 +#: rog-control-center/ui/pages/system.slint:732 msgctxt "ppt_platform_sppt_help" msgid "" "Overall system power limit for sustained operations. Controls total platform " "power consumption over extended periods." msgstr "" -#: rog-control-center/ui/pages/system.slint:747 -#: rog-control-center/ui/pages/system.slint:747 +#: rog-control-center/ui/pages/system.slint:748 +#: rog-control-center/ui/pages/system.slint:748 msgctxt "nv_dynamic_boost" msgid "GPU Power Boost" msgstr "" -#: rog-control-center/ui/pages/system.slint:749 +#: rog-control-center/ui/pages/system.slint:750 msgctxt "nv_dynamic_boost_help" msgid "" "Additional power allocation for GPU dynamic boost. Higher values increase " "GPU performance but generate more heat." msgstr "" -#: rog-control-center/ui/pages/system.slint:765 -#: rog-control-center/ui/pages/system.slint:765 +#: rog-control-center/ui/pages/system.slint:766 +#: rog-control-center/ui/pages/system.slint:766 msgctxt "nv_tgp" msgid "GPU TGP" msgstr "" -#: rog-control-center/ui/pages/system.slint:767 +#: rog-control-center/ui/pages/system.slint:768 msgctxt "nv_tgp_help" msgid "" "Total graphics power limit for the dedicated NVIDIA GPU. Higher values can " "improve performance at the cost of additional heat and power draw." msgstr "" -#: rog-control-center/ui/pages/system.slint:783 -#: rog-control-center/ui/pages/system.slint:783 +#: rog-control-center/ui/pages/system.slint:784 +#: rog-control-center/ui/pages/system.slint:784 msgctxt "nv_temp_target" msgid "GPU Temperature Limit" msgstr "" -#: rog-control-center/ui/pages/system.slint:785 +#: rog-control-center/ui/pages/system.slint:786 msgctxt "nv_temp_target_help" msgid "" "Maximum GPU temperature threshold in Celsius. GPU will throttle to maintain " "temperature below this limit." msgstr "" -#: rog-control-center/ui/pages/system.slint:839 +#: rog-control-center/ui/pages/system.slint:840 msgctxt "PageSystem" msgid "Energy Performance Preference linked to Throttle Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:843 +#: rog-control-center/ui/pages/system.slint:844 msgctxt "PageSystem" msgid "Change EPP based on Throttle Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:851 +#: rog-control-center/ui/pages/system.slint:852 msgctxt "PageSystem" msgid "EPP for Balanced Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:861 +#: rog-control-center/ui/pages/system.slint:862 msgctxt "PageSystem" msgid "EPP for Performance Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:871 +#: rog-control-center/ui/pages/system.slint:872 msgctxt "PageSystem" msgid "EPP for " msgstr "" -#: rog-control-center/ui/pages/system.slint:871 +#: rog-control-center/ui/pages/system.slint:872 msgctxt "PageSystem" msgid " Policy" msgstr "" -#: rog-control-center/ui/pages/system.slint:890 +#: rog-control-center/ui/pages/system.slint:891 msgctxt "PageSystem" msgid "Throttle Policy for power state" msgstr "" -#: rog-control-center/ui/pages/system.slint:896 +#: rog-control-center/ui/pages/system.slint:897 msgctxt "PageSystem" msgid "Throttle Policy on Battery" msgstr "" -#: rog-control-center/ui/pages/system.slint:906 -#: rog-control-center/ui/pages/system.slint:906 +#: rog-control-center/ui/pages/system.slint:907 +#: rog-control-center/ui/pages/system.slint:907 msgctxt "PageSystem" msgid "Enabled" msgstr "" -#: rog-control-center/ui/pages/system.slint:917 +#: rog-control-center/ui/pages/system.slint:918 msgctxt "PageSystem" msgid "Throttle Policy on AC" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Integrated" +#: rog-control-center/ui/pages/battery.slint:24 +msgctxt "PageBattery" +msgid "Battery Information" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Hybrid" +#: rog-control-center/ui/pages/battery.slint:41 +msgctxt "PageBattery" +msgid "Battery Health (Capacity):" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:29 -#: rog-control-center/ui/pages/gpu.slint:29 -msgctxt "PageGPU" -msgid "Ultimate" +#: rog-control-center/ui/pages/battery.slint:54 +msgctxt "PageBattery" +msgid "Status:" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:48 -msgctxt "PageGPU" -msgid "Internal/Discrete GPU" +#: rog-control-center/ui/pages/battery.slint:67 +msgctxt "PageBattery" +msgid "Power Draw:" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:57 -msgctxt "PageGPU" -msgid "This device has no switchable GPU." +#: rog-control-center/ui/pages/battery.slint:80 +msgctxt "PageBattery" +msgid "Time to Full:" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:69 -msgctxt "PageGPU" -msgid "" -"Note: Changes take effect after a reboot. The dropdown always shows the " -"current mode." +#: rog-control-center/ui/pages/battery.slint:80 +msgctxt "PageBattery" +msgid "Time Remaining:" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:76 -msgctxt "PageGPU" -msgid "GPU mode" +#: rog-control-center/ui/pages/battery.slint:93 +msgctxt "PageBattery" +msgid "Charge limit" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:97 -msgctxt "PageGPU" -msgid "Note: Reserved GPU memory changes take effect after a reboot." +#: rog-control-center/ui/pages/slash.slint:54 +msgctxt "PageSlash" +msgid "Slash lighting" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:104 -msgctxt "PageGPU" -msgid "Reserved GPU memory" +#: rog-control-center/ui/pages/slash.slint:59 +msgctxt "PageSlash" +msgid "Enable A-cover lighting" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:118 -msgctxt "PageGPU" -msgid "XG Mobile LED" +#: rog-control-center/ui/pages/slash.slint:67 +msgctxt "PageSlash" +msgid "Brightness" msgstr "" -#: rog-control-center/ui/pages/gpu.slint:131 -msgctxt "PageGPU" -msgid "Disable nvidia-powerd on battery" +#: rog-control-center/ui/pages/slash.slint:80 +msgctxt "PageSlash" +msgid "Animation speed" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:93 +msgctxt "PageSlash" +msgid "Animation" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:113 +msgctxt "PageSlash" +msgid "Visibility triggers" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:118 +msgctxt "PageSlash" +msgid "Show on boot" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:126 +msgctxt "PageSlash" +msgid "Show on shutdown" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:134 +msgctxt "PageSlash" +msgid "Show on sleep" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:142 +msgctxt "PageSlash" +msgid "Show on battery" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:150 +msgctxt "PageSlash" +msgid "Show low-battery warning" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:158 +msgctxt "PageSlash" +msgid "Show when lid is closed" +msgstr "" + +#: rog-control-center/ui/pages/slash.slint:173 +msgctxt "Slash_tip" +msgid "" +"Slash Lighting controls the A-cover LED strip found on supported Zephyrus " +"models." msgstr "" #: rog-control-center/ui/types/aura_types.slint:54 @@ -980,12 +1015,13 @@ msgstr "" #: rog-control-center/ui/types/aura_types.slint:80 #: rog-control-center/ui/types/aura_types.slint:80 msgctxt "Basic aura mode" -msgid "Strobe" +msgid "Rainbow cycle" msgstr "" +#: rog-control-center/ui/types/aura_types.slint:81 #: rog-control-center/ui/types/aura_types.slint:81 msgctxt "Basic aura mode" -msgid "Rainbow" +msgid "Rainbow wave" msgstr "" #: rog-control-center/ui/types/aura_types.slint:82 @@ -1033,77 +1069,87 @@ msgctxt "Basic aura mode" msgid "Flash" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:96 +#: rog-control-center/ui/types/aura_types.slint:91 +msgctxt "Basic aura mode" +msgid "Direct" +msgstr "" + +#: rog-control-center/ui/types/aura_types.slint:92 +msgctxt "Basic aura mode" +msgid "Reactive" +msgstr "" + +#: rog-control-center/ui/types/aura_types.slint:105 msgctxt "Aura zone" msgid "None" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:97 +#: rog-control-center/ui/types/aura_types.slint:106 msgctxt "Aura zone" msgid "Key1" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:98 +#: rog-control-center/ui/types/aura_types.slint:107 msgctxt "Aura zone" msgid "Key2" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:99 +#: rog-control-center/ui/types/aura_types.slint:108 msgctxt "Aura zone" msgid "Key3" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:100 +#: rog-control-center/ui/types/aura_types.slint:109 msgctxt "Aura zone" msgid "Key4" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:101 +#: rog-control-center/ui/types/aura_types.slint:110 msgctxt "Aura zone" msgid "Logo" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:102 +#: rog-control-center/ui/types/aura_types.slint:111 msgctxt "Aura zone" msgid "Lightbar Left" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:103 +#: rog-control-center/ui/types/aura_types.slint:112 msgctxt "Aura zone" msgid "Lightbar Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:106 +#: rog-control-center/ui/types/aura_types.slint:115 msgctxt "Aura direction" msgid "Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:107 +#: rog-control-center/ui/types/aura_types.slint:116 msgctxt "Aura direction" msgid "Left" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:108 +#: rog-control-center/ui/types/aura_types.slint:117 msgctxt "Aura direction" msgid "Up" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:109 +#: rog-control-center/ui/types/aura_types.slint:118 msgctxt "Aura direction" msgid "Down" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:112 +#: rog-control-center/ui/types/aura_types.slint:121 msgctxt "Aura speed" msgid "Low" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:113 +#: rog-control-center/ui/types/aura_types.slint:122 msgctxt "Aura speed" msgid "Medium" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:114 +#: rog-control-center/ui/types/aura_types.slint:123 msgctxt "Aura speed" msgid "High" msgstr "" diff --git a/rog-control-center/ui/pages/slash.slint b/rog-control-center/ui/pages/slash.slint index da151f0df..59ef2f430 100644 --- a/rog-control-center/ui/pages/slash.slint +++ b/rog-control-center/ui/pages/slash.slint @@ -15,6 +15,9 @@ export global SlashPageData { in-out property mode: 0; callback cb_mode(int); + // False for asus::slash sysfs (no power_states); true for legacy HID/USB. + in-out property power_triggers_supported: true; + in-out property show_on_boot: true; callback cb_show_on_boot(bool); in-out property show_on_shutdown: true; @@ -97,7 +100,7 @@ export component PageSlash inherits Rectangle { } } - Rectangle { + if SlashPageData.power_triggers_supported: Rectangle { background: Palette.alternate-background; border-color: Palette.border; border-width: 1px; @@ -111,7 +114,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show on boot"); checked <=> SlashPageData.show_on_boot; toggled => { @@ -119,7 +122,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show on shutdown"); checked <=> SlashPageData.show_on_shutdown; toggled => { @@ -127,7 +130,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show on sleep"); checked <=> SlashPageData.show_on_sleep; toggled => { @@ -135,7 +138,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show on battery"); checked <=> SlashPageData.show_on_battery; toggled => { @@ -143,7 +146,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show low-battery warning"); checked <=> SlashPageData.show_battery_warning; toggled => { @@ -151,7 +154,7 @@ export component PageSlash inherits Rectangle { } } - SystemToggle { + if SlashPageData.power_triggers_supported: SystemToggle { text: @tr("Show when lid is closed"); checked <=> SlashPageData.show_on_lid_closed; toggled => { diff --git a/rog-control-center/ui/types/aura_types.slint b/rog-control-center/ui/types/aura_types.slint index e6d4795fe..8bdd80508 100644 --- a/rog-control-center/ui/types/aura_types.slint +++ b/rog-control-center/ui/types/aura_types.slint @@ -46,9 +46,9 @@ export struct LaptopAuraPower { states: [AuraPowerState], } -// Modes with colour1: Static,Breathe,Star,Rain,Highlight,Laser,Ripple,Pulse,Comet,Flash (excl. Strobe,Rainbow,Nothing) -// Modes with colour2: Breathe, Star only. -// Speed: Breathe,Strobe,Rainbow,Star,Rain,Highlight,Laser,Ripple. Direction: Rainbow only. +// Colour 1: Static, Breathe, Star, Highlight, Laser, Ripple, Pulse, Comet, Flash. +// Colour 2: Breathe, Star. Speed: Breathe through Ripple. Direction: Rainbow wave. +// Index 9 is unused. The page shows only the modes the device reports. export global AuraPageData { in-out property <[string]> power_zone_names: [ @tr("Aura power zone" => "Logo"), @@ -77,8 +77,8 @@ export global AuraPageData { in-out property <[string]> mode_names: [ @tr("Basic aura mode" => "Static"), @tr("Basic aura mode" => "Breathe"), - @tr("Basic aura mode" => "Strobe"), - @tr("Basic aura mode" => "Rainbow"), + @tr("Basic aura mode" => "Rainbow cycle"), + @tr("Basic aura mode" => "Rainbow wave"), @tr("Basic aura mode" => "Star"), @tr("Basic aura mode" => "Rain"), @tr("Basic aura mode" => "Highlight"), @@ -88,8 +88,17 @@ export global AuraPageData { @tr("Basic aura mode" => "Pulse"), @tr("Basic aura mode" => "Comet"), @tr("Basic aura mode" => "Flash"), + @tr("Basic aura mode" => "Direct"), + @tr("Basic aura mode" => "Reactive"), + ]; + // Direct is omitted here: advertise it only when the daemon reports it + // (frame producer / per-key path available). + in-out property <[string]> available_mode_names: [ + @tr("Basic aura mode" => "Static"), + @tr("Basic aura mode" => "Breathe"), + @tr("Basic aura mode" => "Rainbow cycle"), + @tr("Basic aura mode" => "Rainbow wave"), ]; - in-out property <[string]> available_mode_names: [ @tr("Basic aura mode" => "Static"), @tr("Basic aura mode" => "Breathe"), @tr("Basic aura mode" => "Strobe") ]; in-out property current_available_mode: 0; in-out property <[int]> supported_basic_modes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12]; in-out property <[string]> zone_names: [ @@ -127,7 +136,7 @@ export global AuraPageData { in-out property color2; in-out property colorbox2; - out property colour1_enabled: led_mode_data.mode == 0 || led_mode_data.mode == 1 || led_mode_data.mode == 4 || led_mode_data.mode == 6 || led_mode_data.mode == 7 || led_mode_data.mode == 8 || led_mode_data.mode == 10 || led_mode_data.mode == 11 || led_mode_data.mode == 12; + out property colour1_enabled: led_mode_data.mode == 0 || led_mode_data.mode == 1 || led_mode_data.mode == 4 || led_mode_data.mode == 6 || led_mode_data.mode == 7 || led_mode_data.mode == 8 || led_mode_data.mode == 10 || led_mode_data.mode == 11 || led_mode_data.mode == 12 || led_mode_data.mode == 14; out property colour2_enabled: led_mode_data.mode == 1 || led_mode_data.mode == 4; out property speed_enabled: led_mode_data.mode == 1 || led_mode_data.mode == 2 || led_mode_data.mode == 3 || led_mode_data.mode == 4 || led_mode_data.mode == 5 || led_mode_data.mode == 6 || led_mode_data.mode == 7 || led_mode_data.mode == 8; out property direction_enabled: led_mode_data.mode == 3; From f69a6c59aa5ade28332792ee2e1949d40c56f57d Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Thu, 12 Nov 2026 12:45:31 +0100 Subject: [PATCH 6/6] feat(aura): expose synced vs independent chassis zones in RCC Advertise SupportedEffectTargets for All (synced) / Keyboard / Lightbar / Logo. Mirror the last applied zone in LedModeData so the selection sticks. The Zone dropdown only switches the editing target; colour, mode, speed, and direction apply the effect to that chassis node without fan-out from AuraZone::None. Signed-off-by: Marco Scardovi --- asusd/src/aura_laptop/config.rs | 44 +++++++---- asusd/src/aura_laptop/mod.rs | 48 ++++++++++- asusd/src/aura_laptop/trait_impls.rs | 25 ++++++ docs/usage/dynamic-lighting.md | 19 +++-- rog-control-center/src/ui/setup_aura.rs | 79 +++++++++++++++++-- .../translations/en/rog-control-center.po | 49 ++++++------ rog-control-center/ui/pages/aura.slint | 39 ++++----- rog-control-center/ui/types/aura_types.slint | 20 ++++- rog-dbus/src/zbus_aura.rs | 4 + 9 files changed, 242 insertions(+), 85 deletions(-) diff --git a/asusd/src/aura_laptop/config.rs b/asusd/src/aura_laptop/config.rs index febbd812a..e9563e023 100644 --- a/asusd/src/aura_laptop/config.rs +++ b/asusd/src/aura_laptop/config.rs @@ -105,32 +105,36 @@ impl AuraConfig { /// Set the mode data, current mode, and if multizone enabled. /// /// Multipurpose, will accept `AuraEffect` with zones and put in the correct - /// store. + /// store. `builtins` always mirrors the last applied effect (including + /// zone) so D-Bus `LedModeData` / RCC Zone selection stay in sync when + /// applying Keyboard or Lightbar independently. pub fn set_builtin(&mut self, effect: AuraEffect) { self.current_mode = effect.mode; + self.builtins.insert(*effect.mode(), effect.clone()); if effect.zone() == AuraZone::None { - self.builtins.insert(*effect.mode(), effect); self.multizone_on = false; - } else { - if let Some(multi) = self.multizone.as_mut() { - if let Some(fx_vec) = multi.get_mut(effect.mode()) { - for fx in fx_vec.iter_mut() { - if fx.zone == effect.zone { - *fx = effect; - return; - } + return; + } + + if let Some(multi) = self.multizone.as_mut() { + if let Some(fx_vec) = multi.get_mut(effect.mode()) { + for fx in fx_vec.iter_mut() { + if fx.zone == effect.zone { + *fx = effect; + self.multizone_on = true; + return; } - fx_vec.push(effect); - } else { - multi.insert(*effect.mode(), vec![effect]); } + fx_vec.push(effect); } else { - let mut tmp = BTreeMap::new(); - tmp.insert(*effect.mode(), vec![effect]); - self.multizone = Some(tmp); + multi.insert(*effect.mode(), vec![effect]); } - self.multizone_on = true; + } else { + let mut tmp = BTreeMap::new(); + tmp.insert(*effect.mode(), vec![effect]); + self.multizone = Some(tmp); } + self.multizone_on = true; } /// Create a default for the `current_mode` if multizone and no config @@ -282,6 +286,12 @@ mod tests { // This should replace existing config.set_builtin(effect_clone); + assert_eq!( + config.builtins.get(&AuraModeNum::Static).map(|e| e.zone), + Some(AuraZone::Key4), + "builtins mirrors last applied zone for LedModeData" + ); + let res = config.multizone.unwrap(); let sta = res.get(&AuraModeNum::Static).unwrap(); assert_eq!(sta.len(), 4); diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index d3010b0dd..a1ead3d2f 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -675,6 +675,26 @@ pub(super) fn supports_dynamic_zone(zone: rog_aura::AuraZone) -> bool { ) } +/// Chassis effect targets for UI: `None` = synced fan-out; `Key1` = keyboard +/// node; `BarLeft` = lightbar; `Logo` when registered. Not legacy Key2–4 / L-R. +pub(super) fn dynamic_effect_targets( + has_keyboard: bool, + has_lightbar: bool, + has_logo: bool, +) -> Vec { + let mut targets = vec![rog_aura::AuraZone::None]; + if has_keyboard { + targets.push(rog_aura::AuraZone::Key1); + } + if has_lightbar { + targets.push(rog_aura::AuraZone::BarLeft); + } + if has_logo { + targets.push(rog_aura::AuraZone::Logo); + } + targets +} + impl Aura { fn supports_zone(&self, zone: rog_aura::AuraZone) -> bool { match zone { @@ -784,8 +804,8 @@ mod tests { use rog_platform::error::PlatformError; use super::{ - direct_rgb_payload, filter_power_states, is_dl_node_inactive, power_flags_for_state, - supports_dynamic_zone, + direct_rgb_payload, dynamic_effect_targets, filter_power_states, is_dl_node_inactive, + power_flags_for_state, supports_dynamic_zone, }; use crate::error::RogError; @@ -851,6 +871,30 @@ mod tests { assert!(supports_dynamic_zone(AuraZone::Logo)); } + #[test] + fn dynamic_effect_targets_lists_synced_then_present_nodes() { + assert_eq!( + dynamic_effect_targets(true, true, false), + vec![ + AuraZone::None, + AuraZone::Key1, + AuraZone::BarLeft + ] + ); + assert_eq!( + dynamic_effect_targets(true, false, true), + vec![ + AuraZone::None, + AuraZone::Key1, + AuraZone::Logo + ] + ); + assert_eq!( + dynamic_effect_targets(false, false, false), + vec![AuraZone::None] + ); + } + #[test] fn inactive_dl_node_detects_ebusy() { let err = RogError::Platform(PlatformError::IoPath( diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 1899795e8..0985ebcc3 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -137,6 +137,31 @@ impl AuraZbus { Ok(config.support_data.basic_zones.clone()) } + /// Effect apply targets for the UI. + /// + /// Under Dynamic Lighting this is the chassis topology (`None` = synced + /// fan-out, `Key1` = keyboard, `BarLeft` = lightbar, `Logo` when present), + /// not legacy Key2–4 / lightbar-left/right. Legacy Aura keeps + /// `None` plus `supported_basic_zones`. + #[zbus(property)] + async fn supported_effect_targets(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + return Ok(super::dynamic_effect_targets( + self.0.dynamic_kbd.is_some(), + self.0.dynamic_lightbar.is_some(), + self.0.has_dynamic_logo(), + )); + } + let config = self.0.config.lock().await; + let mut targets = vec![AuraZone::None]; + for zone in &config.support_data.basic_zones { + if !targets.contains(zone) { + targets.push(*zone); + } + } + Ok(targets) + } + #[zbus(property)] async fn supported_power_zones(&self) -> Result, ZbErr> { if self.0.has_dynamic_lighting() { diff --git a/docs/usage/dynamic-lighting.md b/docs/usage/dynamic-lighting.md index 53b5da77a..e2b9765e8 100644 --- a/docs/usage/dynamic-lighting.md +++ b/docs/usage/dynamic-lighting.md @@ -34,9 +34,12 @@ remains the legacy TUF/`KeyboardBacklight` path only when no keyboard DL node is present. When any keyboard DL node exists, the legacy platform brightness handle is not kept. -There is no `asus::global` aggregate and no userspace `aura_mode` selector. -Each node accepts its own effect, palette, and direct writes. Whole-device -requests fan out to every present zone (keyboard, lightbar, logo). Intensity +There is no `asus::global` aggregate. Each node accepts its own effect, +palette, and direct writes. Whole-device (`AuraZone::None`) requests fan out +to every present zone (keyboard, lightbar, logo). Independent applies use +chassis aliases (`Key1` → keyboard, `BarLeft` → lightbar, `Logo` → logo). +asusd advertises those choices on `SupportedEffectTargets` for ROG Control +Center; `SupportedBasicZones` stays empty under Dynamic Lighting. Intensity (`brightness`) is written primarily to the keyboard node; lightbar uses `enabled` (and brightness only when the kernel exposes a dimmer). @@ -79,10 +82,12 @@ hidraw Ally power fix is not used when Dynamic Lighting owns the device. The D-Bus API still describes historical keyboard and left/right lightbar subzones. Under Dynamic Lighting those are not advertised -(`supported_basic_zones` is empty). Incoming legacy zone values are accepted -only as aliases: Key1–4 map to the keyboard node, BarLeft/BarRight to the -lightbar, Logo to `asus::logo` when present, and `None` fans out to every -present zone. +(`supported_basic_zones` is empty). `supported_effect_targets` lists the +chassis apply targets instead: `None` (synced), `Key1` (keyboard), `BarLeft` +(lightbar), and `Logo` when that node exists. Incoming legacy zone values are +still accepted as aliases: Key1–4 map to the keyboard node, BarLeft/BarRight +to the lightbar, Logo to `asus::logo` when present, and `None` fans out to +every present zone. ## Other devices diff --git a/rog-control-center/src/ui/setup_aura.rs b/rog-control-center/src/ui/setup_aura.rs index 22b1d17fa..e69b3b0de 100644 --- a/rog-control-center/src/ui/setup_aura.rs +++ b/rog-control-center/src/ui/setup_aura.rs @@ -97,6 +97,7 @@ pub fn setup_aura_page( // Use effect.mode as single source — avoid led_mode() which can fail (try_lock). let restore = aura.led_mode_data().await.ok(); let raw_mode: Option = restore.as_ref().map(|d| d.mode.into()); + let restore_zone: Option = restore.as_ref().map(|d| d.zone.into()); let d_slint = restore.map(|d| d.into()); handle .upgrade_in_event_loop(move |h| { @@ -160,6 +161,41 @@ pub fn setup_aura_page( .ok(); } + if let Ok(targets) = aura.supported_effect_targets().await { + handle + .upgrade_in_event_loop(move |handle| { + let labels: Vec = handle + .global::() + .get_zone_names() + .iter() + .collect(); + let mut raws: Vec = Vec::new(); + let mut names: Vec = Vec::new(); + for zone in &targets { + let raw: i32 = (*zone).into(); + if let Some(label) = labels.get(raw as usize) { + raws.push(raw); + names.push(label.clone()); + } + } + // Enable when there is a real choice (synced + at least one + // chassis node beyond a redundant single-zone keyboard). + let enabled = raws.len() > 2 + || raws.iter().any(|z| { + matches!(*z, 5..=7) // Logo / Lightbar / Lightbar Right + }); + let idx = restore_zone + .and_then(|z| raws.iter().position(|&r| r == z)) + .unwrap_or(0) as i32; + let g = handle.global::(); + g.set_supported_effect_targets(raws.as_slice().into()); + g.set_effect_target_names(names.as_slice().into()); + g.set_effect_targets_enabled(enabled); + g.set_effect_target_index(idx); + }) + .ok(); + } + let proxy = aura.clone(); let weak = handle.clone(); handle @@ -175,7 +211,10 @@ pub fn setup_aura_page( let w = weak.clone(); h.global::().on_apply_led_mode_data(move || { let Some(ui) = w.upgrade() else { return }; - let slint_effect = ui.global::().get_led_mode_data(); + let g = ui.global::(); + let mut slint_effect = g.get_led_mode_data(); + // Zone dropdown is authoritative for chassis target. + slint_effect.zone = g.get_selected_effect_zone(); let raw: rog_aura::AuraEffect = slint_effect.into(); let pp = p.clone(); let t = w.clone(); @@ -184,6 +223,27 @@ pub fn setup_aura_page( show_toast("LED mode applied".into(), "LED mode failed".into(), t, r); }); }); + + // Zone dropdown only switches the editing target. Hardware is + // written when colour / mode / speed / direction apply, using + // selected_effect_zone (not a stale led_mode_data.zone). + let w_zone = weak.clone(); + h.global::() + .on_cb_effect_target_index(move |idx| { + let Some(ui) = w_zone.upgrade() else { return }; + let g = ui.global::(); + let targets: Vec = g.get_supported_effect_targets().iter().collect(); + let Some(&zone) = targets.get(idx as usize) else { + error!("Effect target index out of range: {idx}"); + return; + }; + g.set_effect_target_index(idx); + let mut effect = g.get_led_mode_data(); + effect.zone = zone; + g.set_led_mode_data(effect); + g.set_zone(zone); + }); + h.invoke_external_colour_change(); }) .ok(); @@ -220,14 +280,17 @@ pub fn setup_aura_page( let data = out.into(); stream_handle .upgrade_in_event_loop(move |h| { - h.global::().invoke_update_led_mode_data(data); - let supported: Vec = h - .global::() - .get_supported_basic_modes() - .iter() - .collect(); + let g = h.global::(); + g.invoke_update_led_mode_data(data); + let supported: Vec = + g.get_supported_basic_modes().iter().collect(); let idx = supported.iter().position(|&x| x == raw).unwrap_or(0) as i32; - h.global::().set_current_available_mode(idx); + g.set_current_available_mode(idx); + let targets: Vec = + g.get_supported_effect_targets().iter().collect(); + let zone = g.get_led_mode_data().zone; + let zidx = targets.iter().position(|&t| t == zone).unwrap_or(0) as i32; + g.set_effect_target_index(zidx); h.invoke_external_colour_change(); }) .map_err(|e| error!("{e}")) diff --git a/rog-control-center/translations/en/rog-control-center.po b/rog-control-center/translations/en/rog-control-center.po index f550c7090..92b8b23ad 100644 --- a/rog-control-center/translations/en/rog-control-center.po +++ b/rog-control-center/translations/en/rog-control-center.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-11-12 10:53+0000\n" +"POT-Creation-Date: 2026-11-12 11:54+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -295,27 +295,27 @@ msgctxt "PageAura" msgid "Colour 2" msgstr "" -#: rog-control-center/ui/pages/aura.slint:120 +#: rog-control-center/ui/pages/aura.slint:119 msgctxt "PageAura" msgid "Zone" msgstr "" -#: rog-control-center/ui/pages/aura.slint:141 +#: rog-control-center/ui/pages/aura.slint:132 msgctxt "PageAura" msgid "Direction" msgstr "" -#: rog-control-center/ui/pages/aura.slint:162 +#: rog-control-center/ui/pages/aura.slint:153 msgctxt "PageAura" msgid "Speed" msgstr "" -#: rog-control-center/ui/pages/aura.slint:185 +#: rog-control-center/ui/pages/aura.slint:176 msgctxt "PageAura" msgid "Power Settings" msgstr "" -#: rog-control-center/ui/pages/aura.slint:304 +#: rog-control-center/ui/pages/aura.slint:295 msgctxt "PageAura" msgid "Power Zones" msgstr "" @@ -1079,77 +1079,78 @@ msgctxt "Basic aura mode" msgid "Reactive" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:105 +#: rog-control-center/ui/types/aura_types.slint:107 +#: rog-control-center/ui/types/aura_types.slint:107 msgctxt "Aura zone" -msgid "None" +msgid "All (synced)" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:106 +#: rog-control-center/ui/types/aura_types.slint:108 msgctxt "Aura zone" -msgid "Key1" +msgid "Keyboard" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:107 +#: rog-control-center/ui/types/aura_types.slint:109 msgctxt "Aura zone" msgid "Key2" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:108 +#: rog-control-center/ui/types/aura_types.slint:110 msgctxt "Aura zone" msgid "Key3" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:109 +#: rog-control-center/ui/types/aura_types.slint:111 msgctxt "Aura zone" msgid "Key4" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:110 +#: rog-control-center/ui/types/aura_types.slint:112 msgctxt "Aura zone" msgid "Logo" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:111 +#: rog-control-center/ui/types/aura_types.slint:113 msgctxt "Aura zone" -msgid "Lightbar Left" +msgid "Lightbar" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:112 +#: rog-control-center/ui/types/aura_types.slint:114 msgctxt "Aura zone" msgid "Lightbar Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:115 +#: rog-control-center/ui/types/aura_types.slint:124 msgctxt "Aura direction" msgid "Right" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:116 +#: rog-control-center/ui/types/aura_types.slint:125 msgctxt "Aura direction" msgid "Left" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:117 +#: rog-control-center/ui/types/aura_types.slint:126 msgctxt "Aura direction" msgid "Up" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:118 +#: rog-control-center/ui/types/aura_types.slint:127 msgctxt "Aura direction" msgid "Down" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:121 +#: rog-control-center/ui/types/aura_types.slint:130 msgctxt "Aura speed" msgid "Low" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:122 +#: rog-control-center/ui/types/aura_types.slint:131 msgctxt "Aura speed" msgid "Medium" msgstr "" -#: rog-control-center/ui/types/aura_types.slint:123 +#: rog-control-center/ui/types/aura_types.slint:132 msgctxt "Aura speed" msgid "High" msgstr "" diff --git a/rog-control-center/ui/pages/aura.slint b/rog-control-center/ui/pages/aura.slint index e7c3d2a3c..955fbfcf0 100644 --- a/rog-control-center/ui/pages/aura.slint +++ b/rog-control-center/ui/pages/aura.slint @@ -44,7 +44,7 @@ export component PageAura inherits Rectangle { selected => { AuraPageData.apply_effect({ mode: AuraPageData.supported_basic_modes[self.current-index], - zone: AuraPageData.led_mode_data.zone, + zone: AuraPageData.selected_effect_zone, colour1: AuraPageData.led_mode_data.colour1, colour2: AuraPageData.led_mode_data.colour2, speed: AuraPageData.led_mode_data.speed, @@ -76,7 +76,7 @@ export component PageAura inherits Rectangle { released => { AuraPageData.apply_effect({ mode: AuraPageData.led_mode_data.mode, - zone: AuraPageData.led_mode_data.zone, + zone: AuraPageData.selected_effect_zone, colour1: AuraPageData.color1, colour2: AuraPageData.led_mode_data.colour2, speed: AuraPageData.led_mode_data.speed, @@ -98,7 +98,7 @@ export component PageAura inherits Rectangle { released => { AuraPageData.apply_effect({ mode: AuraPageData.led_mode_data.mode, - zone: AuraPageData.led_mode_data.zone, + zone: AuraPageData.selected_effect_zone, colour1: AuraPageData.led_mode_data.colour1, colour2: AuraPageData.color2, speed: AuraPageData.led_mode_data.speed, @@ -115,25 +115,16 @@ export component PageAura inherits Rectangle { spacing: 10px; min-height: 80px; max-height: 90px; - RogItem { - VerticalBox { - Text { text: @tr("Zone"); vertical-alignment: TextVerticalAlignment.center; horizontal-alignment: TextHorizontalAlignment.center; } - ComboBox { - enabled: false; - current_index <=> AuraPageData.zone; - current_value: AuraPageData.zone_names[self.current-index]; - model <=> AuraPageData.zone_names; - selected => { - AuraPageData.apply_effect({ - mode: AuraPageData.led_mode_data.mode, - zone: self.current-index, - colour1: AuraPageData.led_mode_data.colour1, - colour2: AuraPageData.led_mode_data.colour2, - speed: AuraPageData.led_mode_data.speed, - direction: AuraPageData.led_mode_data.direction, - }); - } - } + SystemDropdown { + text: @tr("Zone"); + enabled: AuraPageData.effect_targets_enabled; + // All (synced) vs Keyboard / Lightbar / Logo. Changing the + // target alone does not write LEDs; colour/mode/speed do. + current_index <=> AuraPageData.effect_target_index; + current_value: AuraPageData.effect_target_names[self.current_index]; + model <=> AuraPageData.effect_target_names; + selected(idx) => { + AuraPageData.cb_effect_target_index(idx); } } RogItem { @@ -147,7 +138,7 @@ export component PageAura inherits Rectangle { selected => { AuraPageData.apply_effect({ mode: AuraPageData.led_mode_data.mode, - zone: AuraPageData.led_mode_data.zone, + zone: AuraPageData.selected_effect_zone, colour1: AuraPageData.led_mode_data.colour1, colour2: AuraPageData.led_mode_data.colour2, speed: AuraPageData.led_mode_data.speed, @@ -168,7 +159,7 @@ export component PageAura inherits Rectangle { selected => { AuraPageData.apply_effect({ mode: AuraPageData.led_mode_data.mode, - zone: AuraPageData.led_mode_data.zone, + zone: AuraPageData.selected_effect_zone, colour1: AuraPageData.led_mode_data.colour1, colour2: AuraPageData.led_mode_data.colour2, speed: self.current-index, diff --git a/rog-control-center/ui/types/aura_types.slint b/rog-control-center/ui/types/aura_types.slint index 8bdd80508..f15ada18d 100644 --- a/rog-control-center/ui/types/aura_types.slint +++ b/rog-control-center/ui/types/aura_types.slint @@ -101,16 +101,25 @@ export global AuraPageData { ]; in-out property current_available_mode: 0; in-out property <[int]> supported_basic_modes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12]; + // Full AuraZone enum labels (index == AuraZone discriminant). Filtered + // `effect_target_names` is what the Zone ComboBox shows. in-out property <[string]> zone_names: [ - @tr("Aura zone" => "None"), - @tr("Aura zone" => "Key1"), + @tr("Aura zone" => "All (synced)"), + @tr("Aura zone" => "Keyboard"), @tr("Aura zone" => "Key2"), @tr("Aura zone" => "Key3"), @tr("Aura zone" => "Key4"), @tr("Aura zone" => "Logo"), - @tr("Aura zone" => "Lightbar Left"), + @tr("Aura zone" => "Lightbar"), @tr("Aura zone" => "Lightbar Right"), ]; + // AuraZone discriminants for the Zone ComboBox (from asusd). + in-out property <[int]> supported_effect_targets: [0]; + in-out property <[string]> effect_target_names: [ + @tr("Aura zone" => "All (synced)"), + ]; + in-out property effect_targets_enabled: false; + in-out property effect_target_index: 0; in-out property <[string]> direction_names: [ @tr("Aura direction" => "Right"), @tr("Aura direction" => "Left"), @@ -144,6 +153,10 @@ export global AuraPageData { callback apply_led_mode_data(); callback apply_effect(AuraEffect); apply_effect(e) => { led_mode_data = e; apply_led_mode_data(); } + // Chassis target from the Zone dropdown (source of truth for applies). + out property selected_effect_zone: supported_effect_targets[effect_target_index]; + // Switch editing target only — do not write LEDs until colour/mode/speed change. + callback cb_effect_target_index(int); in-out property zone; in-out property speed; in-out property direction; @@ -158,6 +171,7 @@ export global AuraPageData { color2 = d.colour2; colorbox1 = d.colour1; colorbox2 = d.colour2; + // effect_target_index is synced from Rust when targets are known. } callback cb_hex_from_colour(color) -> string; callback cb_hex_to_colour(string) -> color; diff --git a/rog-dbus/src/zbus_aura.rs b/rog-dbus/src/zbus_aura.rs index b2693d6a9..a8e0e56c5 100644 --- a/rog-dbus/src/zbus_aura.rs +++ b/rog-dbus/src/zbus_aura.rs @@ -81,6 +81,10 @@ pub trait Aura { #[zbus(property)] fn supported_basic_zones(&self) -> zbus::Result>; + /// SupportedEffectTargets property (synced / keyboard / lightbar / logo) + #[zbus(property)] + fn supported_effect_targets(&self) -> zbus::Result>; + /// SupportedPowerZones property #[zbus(property)] fn supported_power_zones(&self) -> zbus::Result>;