diff --git a/Cargo.lock b/Cargo.lock index 50993ad42..81e08ba1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4795,7 +4795,6 @@ dependencies = [ name = "rog_scsi" version = "6.4.0" dependencies = [ - "libc", "ron", "serde", "thiserror 2.0.20", @@ -4816,7 +4815,6 @@ dependencies = [ name = "rog_slash" version = "6.4.0" dependencies = [ - "dmi_id", "serde", "thiserror 2.0.20", "zbus", diff --git a/Cargo.toml b/Cargo.toml index 1de9691a4..8f727797e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,6 @@ glam = { version = "^0.33.5", features = ["serde"] } image = "=0.25.10" inotify = "^0.11.5" ksni = { version = "^0.3.6", default-features = false, features = ["async-io"] } -libc = "^0.2.189" log = "^0.4.33" logind-zbus = { version = "^5.3.2" } mio = "^1.2.2" diff --git a/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/src/aura_anime/mod.rs b/asusd/src/aura_anime/mod.rs index 5d622f3ab..0d3f6230b 100644 --- a/asusd/src/aura_anime/mod.rs +++ b/asusd/src/aura_anime/mod.rs @@ -9,12 +9,12 @@ use std::thread::sleep; use config_traits::StdConfig; use log::{debug, error, info, warn}; +use rog_anime::error::AnimeError; use rog_anime::usb::{ Brightness, pkt_flush, pkt_set_brightness, pkt_set_enable_display, pkt_set_enable_powersave_anim, pkts_for_init, }; use rog_anime::{ActionData, AnimeDataBuffer, AnimePacketType}; -use rog_platform::hid_raw::HidRaw; use rog_platform::usb_raw::USBRaw; use tokio::sync::Mutex; @@ -23,7 +23,6 @@ use crate::error::RogError; #[derive(Debug, Clone)] pub struct AniMe { - hid: Option>>, usb: Option>>, config: Arc>, cache: AniMeConfigCached, @@ -34,13 +33,8 @@ pub struct AniMe { } impl AniMe { - pub fn new( - hid: Option>>, - usb: Option>>, - config: Arc>, - ) -> Self { + pub fn new(usb: Option>>, config: Arc>) -> Self { Self { - hid, usb, config, cache: AniMeConfigCached::default(), @@ -78,11 +72,10 @@ impl AniMe { } pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; - } + let Some(usb) = &self.usb else { + return Err(RogError::Anime(AnimeError::NoDevice)); + }; + usb.lock().await.write_bytes(message)?; Ok(()) } diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index f6ebef2ca..614c658ad 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -2,10 +2,12 @@ use std::sync::Arc; use config::AuraConfig; use config_traits::StdConfig; -use log::info; +use log::{debug, info}; use rog_aura::keyboard::{AuraLaptopUsbPackets, LedUsbPackets}; use rog_aura::usb::{AURA_LAPTOP_LED_APPLY, AURA_LAPTOP_LED_SET}; use rog_aura::{AURA_LAPTOP_LED_MSG_LEN, AuraDeviceType, AuraEffect, LedBrightness, PowerZones}; +use rog_platform::DynamicLed; +use rog_platform::error::PlatformError; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -17,14 +19,50 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Aura { + pub dynamic_global: Option>>, + pub dynamic_kbd: Option>>, + pub dynamic_lightbar: Option>>, pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, } impl Aura { + #[must_use] + pub fn has_dynamic_lighting(&self) -> bool { + self.dynamic_kbd.is_some() + || self.dynamic_global.is_some() + || self.dynamic_lightbar.is_some() + } + + async fn set_asus_topology(&self, mode: &str) -> Result<(), RogError> { + for led in [ + self.dynamic_global.as_ref(), + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + ] + .into_iter() + .flatten() + { + let led = led.lock().await; + if led.has_asus_aura_mode() { + led.set_asus_aura_mode(mode)?; + return Ok(()); + } + } + Err(RogError::MissingFunction( + "ASUS Dynamic Lighting topology control is unavailable".into(), + )) + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { + // With a chassis lightbar, prefer split so keyboard and lightbar are + // independently writable (matches kernel auto→split). Unified remains + // available when userspace wants a single global effect. + if self.dynamic_lightbar.is_some() { + self.set_asus_topology("split").await?; + } Ok(()) } @@ -35,14 +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(()) } @@ -93,82 +128,386 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { - if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { - if let Some(platform) = &self.backlight { - let buf = [ - 1, mode.mode as u8, mode.colour1.r, mode.colour1.g, mode.colour1.b, - mode.speed as u8, - ]; - platform.lock().await.set_kbd_rgb_mode(&buf)?; + // Priority: Dynamic Lighting sysfs interface. + // Key1–4 map to the whole keyboard node; BarLeft/BarRight to lightbar. + // Legacy Logo / other zones remain unsupported under DL. + if self.has_dynamic_lighting() && !supports_dynamic_zone(mode.zone) { + return Err(RogError::MissingFunction( + "Dynamic Lighting exposes keyboard/lightbar topology, not legacy Aura subzones" + .into(), + )); + } + if self.has_dynamic_lighting() + && let Some(eff_str) = mode.mode.to_dynamic_effect_str() + { + let speed = mode.speed.to_dynamic_speed(); + let dir_str = mode.direction.to_dynamic_direction_str(); + let palette = mode.to_dynamic_palette(); + + let apply_to_led = |led: &DynamicLed| -> Result { + if !led.supports_effect(eff_str)? { + return Ok(false); + } + if led.has_speed() { + match led.set_supported_speed(speed) { + Ok(()) => {} + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + return Ok(false); + } + return Err(err); + } + } + } + if led.has_direction() { + match led.set_supported_direction(dir_str) { + Ok(()) => {} + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + return Ok(false); + } + return Err(err); + } + } + } + if led.has_effects_palette() && !palette.is_empty() { + match led.set_palette_colors(&palette) { + Ok(()) => {} + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + return Ok(false); + } + return Err(err); + } + } + } + match led.set_effect(eff_str) { + Ok(()) => Ok(true), + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + Ok(false) + } else { + Err(err) + } + } + } + }; + + match mode.zone { + rog_aura::AuraZone::BarLeft | rog_aura::AuraZone::BarRight => { + self.set_asus_topology("split").await?; + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led)? { + return Ok(()); + } + } + } + rog_aura::AuraZone::None => { + // Whole-device: only unified topology uses aura:global. + // Otherwise (split / auto→split) fan out to keyboard and + // lightbar so both zones get the same effect. + let unified = if let Some(global) = &self.dynamic_global { + let led = global.lock().await; + led.has_asus_aura_mode() + && led.get_asus_aura_mode().ok().as_deref() == Some("unified") + } else { + false + }; + if unified && let Some(global) = &self.dynamic_global { + let global_led = global.lock().await; + if apply_to_led(&global_led)? { + return Ok(()); + } + } + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + let mut any = apply_to_led(&kbd_led)?; + if let Some(lb) = &self.dynamic_lightbar { + let lb_led = lb.lock().await; + any |= apply_to_led(&lb_led)?; + } + if any { + return Ok(()); + } + } + } + // Key1–4: whole keyboard node under DL (no legacy 4-zone split). + _ => { + if self.dynamic_lightbar.is_some() { + self.set_asus_topology("split").await?; + } + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + if apply_to_led(&kbd_led)? { + return Ok(()); + } + } + } } + } + + // When Dynamic Lighting is active, do not fall back to raw hidraw or TUF + // platform + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "Dynamic lighting mode or zone not supported by kernel".to_string(), + )); + } + + // Fallback is selected only when no valid Dynamic Lighting node exists. + if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) + && let Some(platform) = &self.backlight + { + let buf = [ + 1, mode.mode as u8, mode.colour1.r, mode.colour1.g, mode.colour1.b, + mode.speed as u8, + ]; + platform.lock().await.set_kbd_rgb_mode(&buf)?; + return Ok(()); } else if let Some(hid_raw) = &self.hid { - // Some keyboard controllers (e.g. G533QS firmware) silently drop - // short HID writes and only honour packets matching the OUTPUT - // report size declared in the HID descriptor (64 bytes for the - // 0x5d report). Pad effect/SET/APPLY here so we keep working on - // newer Strix/Zephyrus models without regressing older laptops. const PADDED_LEN: usize = 64; let bytes: [u8; AURA_LAPTOP_LED_MSG_LEN] = mode.into(); let mut effect_padded = [0u8; PADDED_LEN]; - effect_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&bytes); + effect_padded[..bytes.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); + set_padded[..AURA_LAPTOP_LED_SET.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); + apply_padded[..AURA_LAPTOP_LED_APPLY.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); + return Ok(()); } - Ok(()) + Err(RogError::NoAuraKeyboard) + } + + /// Nodes that hold live brightness for the current ASUS topology. + /// Kernel `auto` resolves to split, so keyboard/lightbar are canonical. + async fn topology_brightness_slots(&self) -> [Option<&Arc>>; 3] { + let mut mode = None; + for slot in [ + self.dynamic_global.as_ref(), + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + ] + .into_iter() + .flatten() + { + let led = slot.lock().await; + if led.has_asus_aura_mode() { + mode = led.get_asus_aura_mode().ok(); + break; + } + } + match mode.as_deref() { + Some("unified") => [ + self.dynamic_global.as_ref(), + None, + None, + ], + Some("split") | Some("auto") => [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + None, + ], + _ => [ + self.dynamic_kbd.as_ref(), + self.dynamic_lightbar.as_ref(), + self.dynamic_global.as_ref(), + ], + } } - pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + /// Read the current brightness as a 4-step [`LedBrightness`] level. + /// + /// Dynamic Lighting nodes may advertise `max_brightness > 3`; those values + /// are scaled back to Off/Low/Med/High. LED class reads do not return + /// `-EBUSY`, so inactive nodes can still look readable — follow `aura_mode` + /// (kernel `auto` is split) instead of trusting the first node that replies. + pub async fn get_brightness(&self) -> Result { + for slot in self.topology_brightness_slots().await.into_iter().flatten() { + let led = slot.lock().await; + let max = match led.get_max_brightness() { + Ok(m) => m, + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + continue; + } + return Err(err); + } + }; + match led.get_brightness() { + Ok(value) => return Ok(LedBrightness::from_scaled(value, max)), + Err(e) => { + let err = RogError::from(e); + if is_dl_node_inactive(&err) { + continue; + } + return Err(err); + } + } + } if let Some(backlight) = &self.backlight { - backlight.lock().await.set_brightness(value)?; - return Ok(()); + let value = backlight.lock().await.get_brightness()?; + return Ok(value.into()); } Err(RogError::MissingFunction( "No LED backlight control available".to_string(), )) } + /// Set keyboard/Aura brightness from a 4-step [`LedBrightness`] level. + /// + /// When Dynamic Lighting nodes are present the value is scaled to each + /// *active* node's `max_brightness`. Under ASUS `aura_mode=unified` only + /// `aura:global` accepts writes (`keyboard`/`lightbar` return `-EBUSY`); + /// under `split` the reverse is true. Inactive-node errors are skipped so + /// Fn-key brightness works in either topology. The legacy + /// `KeyboardBacklight` (0..=3) is also updated when present. + pub async fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + let mut applied = false; + for led in [ + &self.dynamic_global, + &self.dynamic_kbd, + &self.dynamic_lightbar, + ] + .into_iter() + .flatten() + { + let led = led.lock().await; + let max = led.get_max_brightness()?; + match led.set_brightness(brightness.to_scaled(max)) { + Ok(()) => applied = true, + Err(e) => { + let err = RogError::from(e); + if !is_dl_node_inactive(&err) { + return Err(err); + } + } + } + } + + if let Some(backlight) = &self.backlight { + match backlight.lock().await.set_brightness(brightness.into()) { + Ok(()) => applied = true, + Err(e) if applied => { + debug!("WMI kbd backlight brightness sync failed: {e}"); + } + Err(e) => return Err(e.into()), + } + } + + if applied { + Ok(()) + } else { + Err(RogError::MissingFunction( + "No LED backlight control available".to_string(), + )) + } + } + /// Set combination state for boot animation/sleep animation/all leds/keys /// leds/side leds LED active pub async fn set_power_states(&self, config: &AuraConfig) -> Result<(), RogError> { - if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { - if let Some(backlight) = &self.backlight { - // TODO: tuf bool array - let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; + if self.has_dynamic_lighting() { + let mut requested = Vec::new(); + for state in &config.enabled.states { + if state.boot { + requested.push("boot"); + } + if state.awake { + requested.push("awake"); + } + if state.sleep { + requested.push("sleep"); + } + if state.shutdown { + requested.push("shutdown"); + } + } + let mut applied = false; + let mut saw_power_states = false; + let mut unsupported_request = false; + for slot in [ + &self.dynamic_global, + &self.dynamic_kbd, + &self.dynamic_lightbar, + ] { + let Some(led) = slot else { + continue; + }; + let led = led.lock().await; + if !led.has_power_states() { + continue; + } + saw_power_states = true; + let states = filter_power_states(&requested, &led.get_power_states_index()?); + if states.is_empty() && !requested.is_empty() { + unsupported_request = true; + continue; + } + match led.set_power_states(&states.join(" ")) { + Ok(()) => applied = true, + Err(e) => { + let err = RogError::from(e); + if !is_dl_node_inactive(&err) { + return Err(err); + } + } + } + } + if applied { + return Ok(()); + } + if !saw_power_states { + return Err(RogError::MissingFunction( + "Dynamic Lighting node does not expose power_states".into(), + )); + } + if unsupported_request { + return Err(RogError::MissingFunction( + "No requested power state is supported by this Dynamic Lighting node".into(), + )); } + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted power_states".into(), + )); + } + + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && let Some(backlight) = &self.backlight + { + // TODO: tuf bool array + let buf = config.enabled.to_bytes(config.led_type); + backlight.lock().await.set_kbd_rgb_state(&buf)?; } else if let Some(hid_raw) = &self.hid { let hid_raw = hid_raw.lock().await; if let Some(p) = config.enabled.states.first() && p.zone == PowerZones::Ally { - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xd1, 0x09, 0x01, p.new_to_byte() as u8, - 0x0, - 0x0, - ]; - hid_raw.write_bytes(&msg)?; + 0, + 0, + ])?; return Ok(()); } - let bytes = config.enabled.to_bytes(config.led_type); - let msg = [ + hid_raw.write_bytes(&[ 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], - ]; - hid_raw.write_bytes(&msg)?; + ])?; } Ok(()) } @@ -186,56 +525,222 @@ impl Aura { config.write(); } - let pkt_type = effect[0][1]; - const PER_KEY_TYPE: u8 = 0xbc; + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && !self.has_dynamic_lighting() + && let Some(tuf) = &self.backlight + { + for row in effect.iter() { + let rgb = row.get(9..12).ok_or(PlatformError::InvalidValue)?; + let [r, g, b] = rgb else { + return Err(PlatformError::InvalidValue.into()); + }; + tuf.lock().await.set_kbd_rgb_mode(&[ + 0, 0, *r, *g, *b, 0, + ])?; + } + return Ok(()); + } + + let dynamic_leds = [ + self.dynamic_kbd.as_ref(), + self.dynamic_global.as_ref(), + ]; + for maybe_led in dynamic_leds.into_iter().flatten() { + let dynamic = maybe_led.lock().await; + let led_count = dynamic.get_led_count()? as usize; + let rgb_buf = direct_rgb_payload(effect, led_count)?; + if !rgb_buf.is_empty() { + match dynamic.write_direct(&rgb_buf) { + Ok(()) => { + config.per_key_mode_active = true; + return Ok(()); + } + Err(PlatformError::IoPath(_, ref e)) + if e.kind() == std::io::ErrorKind::ResourceBusy + || e.raw_os_error() == Some(16) => + { + debug!( + "Dynamic lighting node '{}' busy (-EBUSY), trying alternate", + dynamic.name() + ); + continue; + } + Err(e) => return Err(e.into()), + } + } + } + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "No active Dynamic Lighting node accepted the direct frame".into(), + )); + } if let Some(hid_raw) = &self.hid { + let first = effect.first().ok_or(PlatformError::InvalidValue)?; + let packet_type = *first.get(1).ok_or(PlatformError::InvalidValue)?; + if effect.iter().any(|row| { + row.first() != Some(&0x5d) + || row.len() > 64 + || (packet_type == 0xbc && row.len() != 64) + }) { + return Err(PlatformError::InvalidValue.into()); + } let hid_raw = hid_raw.lock().await; - if pkt_type != PER_KEY_TYPE { + if packet_type != 0xbc { config.per_key_mode_active = false; - hid_raw.write_bytes(&effect[0])?; + hid_raw.write_bytes(first)?; hid_raw.write_bytes(&AURA_LAPTOP_LED_SET)?; - // hid_raw.write_bytes(&LED_APPLY)?; } else { if !config.per_key_mode_active { - let init = LedUsbPackets::get_init_msg(); - hid_raw.write_bytes(&init)?; + hid_raw.write_bytes(&LedUsbPackets::get_init_msg())?; config.per_key_mode_active = true; } - for row in effect.iter() { + for row in effect { hid_raw.write_bytes(row)?; } } - } else if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) - && let Some(tuf) = &self.backlight - { - for row in effect.iter() { - let r = row[9]; - let g = row[10]; - let b = row[11]; - tuf.lock().await.set_kbd_rgb_mode(&[ - 0, 0, r, g, b, 0, - ])?; - } + return Ok(()); } - Ok(()) + + Err(RogError::NoAuraKeyboard) } pub async fn fix_ally_power(&mut self) -> Result<(), RogError> { - if self.config.lock().await.led_type == AuraDeviceType::Ally - && let Some(hid_raw) = &self.hid - { + let needs_fix = { + let config = self.config.lock().await; + config.led_type == AuraDeviceType::Ally && config.ally_fix.is_none() + }; + if needs_fix && let Some(hid_raw) = &self.hid { + hid_raw.lock().await.write_bytes(&[ + 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, + ])?; let mut config = self.config.lock().await; - if config.ally_fix.is_none() { - let msg = [ - 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, - ]; - hid_raw.lock().await.write_bytes(&msg)?; - info!("Reset Ally power settings to base"); - config.ally_fix = Some(true); - } + config.ally_fix = Some(true); config.write(); } Ok(()) } } + +fn direct_rgb_payload( + packets: &AuraLaptopUsbPackets, + led_count: usize, +) -> Result, PlatformError> { + let expected = led_count + .checked_mul(3) + .ok_or(PlatformError::InvalidValue)?; + let first = packets.first().ok_or(PlatformError::InvalidValue)?; + let mut rgb = Vec::with_capacity(expected); + if packets.len() == 1 && led_count == 4 { + rgb.extend_from_slice(first.get(9..21).ok_or(PlatformError::InvalidValue)?); + } else { + for packet in packets { + if packet.get(1).copied() != Some(0xbc) { + return Err(PlatformError::InvalidValue); + } + let count = *packet.get(7).ok_or(PlatformError::InvalidValue)? as usize; + let payload_len = count.checked_mul(3).ok_or(PlatformError::InvalidValue)?; + let end = 9usize + .checked_add(payload_len) + .ok_or(PlatformError::InvalidValue)?; + rgb.extend_from_slice(packet.get(9..end).ok_or(PlatformError::InvalidValue)?); + } + } + if rgb.len() != expected { + return Err(PlatformError::InvalidValue); + } + Ok(rgb) +} + +pub(super) fn supports_dynamic_zone(zone: rog_aura::AuraZone) -> bool { + matches!( + zone, + rog_aura::AuraZone::None + | rog_aura::AuraZone::Key1 + | rog_aura::AuraZone::Key2 + | rog_aura::AuraZone::Key3 + | rog_aura::AuraZone::Key4 + | rog_aura::AuraZone::BarLeft + | rog_aura::AuraZone::BarRight + ) +} + +/// Kernel returns `-EBUSY` when writing a Dynamic Lighting node that is inactive +/// for the current `aura_mode` (`unified` vs `split`). +fn is_dl_node_inactive(err: &RogError) -> bool { + let io = match err { + RogError::Platform(PlatformError::IoPath(_, e)) => Some(e), + RogError::Platform(PlatformError::Io(e)) => Some(e), + RogError::Write(_, e) | RogError::Path(_, e) | RogError::Io(e) => Some(e), + _ => None, + }; + io.is_some_and(|e| e.kind() == std::io::ErrorKind::ResourceBusy) +} + +fn filter_power_states<'a>(requested: &[&'a str], supported: &str) -> Vec<&'a str> { + let supported: std::collections::HashSet<_> = supported.split_whitespace().collect(); + requested + .iter() + .copied() + .filter(|state| supported.contains(state)) + .collect() +} + +#[cfg(test)] +mod tests { + use std::io::{Error, ErrorKind}; + + use rog_aura::AuraZone; + use rog_platform::error::PlatformError; + + use super::{ + direct_rgb_payload, filter_power_states, is_dl_node_inactive, supports_dynamic_zone, + }; + use crate::error::RogError; + + #[test] + fn direct_payload_requires_exact_size() { + let mut packet = vec![0; 15]; + packet[1] = 0xbc; + packet[7] = 2; + packet[9..15].copy_from_slice(&[ + 1, 2, 3, 4, 5, 6, + ]); + assert_eq!( + direct_rgb_payload(&vec![packet.clone()], 2).unwrap().len(), + 6 + ); + assert!(direct_rgb_payload(&vec![packet], 3).is_err()); + assert!(direct_rgb_payload(&Vec::new(), 1).is_err()); + } + + #[test] + fn power_states_are_intersected_with_capabilities() { + assert_eq!( + filter_power_states( + &[ + "boot", "awake", "shutdown" + ], + "awake sleep" + ), + ["awake"] + ); + } + + #[test] + fn dynamic_topology_allows_keyboard_and_lightbar_proxies() { + assert!(supports_dynamic_zone(AuraZone::None)); + assert!(supports_dynamic_zone(AuraZone::Key1)); + assert!(supports_dynamic_zone(AuraZone::BarLeft)); + assert!(!supports_dynamic_zone(AuraZone::Logo)); + } + + #[test] + fn inactive_dl_node_detects_ebusy() { + let err = RogError::Platform(PlatformError::IoPath( + "aura:keyboard/brightness".into(), + Error::new(ErrorKind::ResourceBusy, "Device or resource busy"), + )); + assert!(is_dl_node_inactive(&err)); + } +} diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 90629044d..8635f1db8 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -9,7 +9,7 @@ use zbus::object_server::SignalEmitter; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; -use super::Aura; +use super::{Aura, supports_dynamic_zone}; use crate::error::RogError; use crate::{CtrlTask, Reloadable}; @@ -27,11 +27,8 @@ impl AuraZbus { pub async fn start_tasks( mut self, connection: &Connection, - // _signal_ctx: SignalEmitter<'static>, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); - // let signal_ctx = signal_ctx.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -39,11 +36,11 @@ impl AuraZbus { .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - // TODO: skip this until we keep handles to tasks so they can be killed - // task.create_tasks(signal_ctx).await - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } @@ -61,25 +58,17 @@ impl AuraZbus { /// Return the current LED brightness #[zbus(property)] async fn brightness(&self) -> Result { - if let Some(bl) = self.0.backlight.as_ref() { - return Ok(bl.lock().await.get_brightness().map(|n| n.into())?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + Ok(self.0.get_brightness().await?) } - /// Set the keyboard brightness level (0-3) + /// Set the keyboard brightness level (Off/Low/Med/High) #[zbus(property)] async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { - if let Some(bl) = self.0.backlight.as_ref() { - let res = bl.lock().await.set_brightness(brightness.into()); - if res.is_ok() { - let mut config = self.0.config.lock().await; - config.brightness = brightness; - config.write(); - } - return Ok(res?); - } - Err(ZbErr::Failed("No sysfs brightness control".to_string())) + self.0.set_brightness(brightness).await?; + let mut config = self.0.config.lock().await; + config.brightness = brightness; + config.write(); + Ok(()) } /// Total levels of brightness available @@ -97,17 +86,55 @@ impl AuraZbus { #[zbus(property)] async fn supported_basic_modes(&self) -> Result, ZbErr> { let config = self.0.config.lock().await; + if self.0.has_dynamic_lighting() { + let led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + if let Some(led) = led_lock { + let mut modes = Vec::new(); + for mode in config.builtins.keys() { + if let Some(eff_str) = mode.to_dynamic_effect_str() + && led.supports_effect(eff_str)? + { + modes.push(*mode); + } + } + return Ok(modes); + } + } Ok(config.builtins.keys().cloned().collect()) } #[zbus(property)] async fn supported_basic_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + // The kernel nodes split keyboard from lightbar, but do not model + // the historical four keyboard or left/right lightbar zones. + return Ok(Vec::new()); + } let config = self.0.config.lock().await; Ok(config.support_data.basic_zones.clone()) } #[zbus(property)] async fn supported_power_zones(&self) -> Result, ZbErr> { + if self.0.has_dynamic_lighting() { + let has_power_states = if let Some(global) = &self.0.dynamic_global { + global.lock().await.has_power_states() + } else if let Some(kbd) = &self.0.dynamic_kbd { + kbd.lock().await.has_power_states() + } else { + false + }; + 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 +143,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 +162,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 +191,35 @@ impl AuraZbus { #[zbus(property)] async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> { let mut config = self.0.config.lock().await; - if !config.support_data.basic_modes.contains(&effect.mode) - || effect.zone != AuraZone::None - && !config.support_data.basic_zones.contains(&effect.zone) - { + let (is_mode_supported, is_zone_supported) = if self.0.has_dynamic_lighting() { + let mode_ok = if let Some(eff_str) = effect.mode.to_dynamic_effect_str() { + let led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + match led_lock { + Some(led) => led.supports_effect(eff_str)?, + None => false, + } + } else { + false + }; + let zone_ok = supports_dynamic_zone(effect.zone); + (mode_ok, zone_ok) + } else { + ( + config.support_data.basic_modes.contains(&effect.mode), + effect.zone == AuraZone::None + || config.support_data.basic_zones.contains(&effect.zone) + || (self.0.dynamic_lightbar.is_some() + && matches!(effect.zone, AuraZone::BarLeft | AuraZone::BarRight)), + ) + }; + + if !is_mode_supported || !is_zone_supported { return Err(ZbErr::NotSupported(format!( "The Aura effect is not supported: {effect:?}" ))); @@ -181,7 +231,7 @@ impl AuraZbus { if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if let Err(e) = self.0.set_brightness(config.brightness.into()).await { + if let Err(e) = self.0.set_brightness(config.brightness).await { log::warn!("Could not set keyboard backlight brightness: {e}"); } config.set_builtin(effect); @@ -250,35 +300,16 @@ impl CtrlTask for AuraZbus { async move { if !sleeping { info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner1.backlight { - backlight - .lock() - .await - .set_brightness(inner1.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + let brightness = inner1.config.lock().await.brightness; + if let Err(e) = inner1.set_brightness(brightness).await { + error!("CtrlKbdLedTask: {e}"); } let mut config = inner1.config.lock().await; - inner1 - .write_current_config_mode(&mut config) - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); - } else if sleeping { - inner1 - .update_config() - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if let Err(e) = inner1.write_current_config_mode(&mut config).await { + error!("CtrlKbdLedTask: {e}"); + } + } else if let Err(e) = inner1.update_config().await { + error!("CtrlKbdLedTask: {e}"); } } }, @@ -286,17 +317,9 @@ impl CtrlTask for AuraZbus { let inner3 = inner3.clone(); async move { info!("CtrlKbdLedTask reloading brightness and modes"); - if let Some(backlight) = &inner3.backlight { - // unwrap as we want to bomb out of the task - backlight - .lock() - .await - .set_brightness(inner3.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + let brightness = inner3.config.lock().await.brightness; + if let Err(e) = inner3.set_brightness(brightness).await { + error!("CtrlKbdLedTask: {e}"); } } }, @@ -311,27 +334,6 @@ impl CtrlTask for AuraZbus { ) .await; - // let ctrl2 = self.0.clone(); - // let ctrl = self.0.lock().await; - // if ctrl.led_node.has_brightness_control() { - // let watch = ctrl.led_node.monitor_brightness()?; - // tokio::spawn(async move { - // let mut buffer = [0; 32]; - // watch - // .into_event_stream(&mut buffer) - // .unwrap() - // .for_each(|_| async { - // if let Some(lock) = ctrl2.try_lock() { - // load_save(true, lock).unwrap(); // unwrap as we want - // // to - // // bomb out of the - // // task - // } - // }) - // .await; - // }); - // } - Ok(()) } } diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d809bd744..4096703fa 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; @@ -32,26 +32,26 @@ const MOD_NAME: &str = "aura"; pub fn filename_partial(parent: &Device) -> Option { if let Some(id_product) = parent.attribute_value("idProduct") { let id_product = id_product.to_string_lossy(); - let mut path = if let Some(devnum) = parent.attribute_value("devnum") { - let devnum = devnum.to_string_lossy(); - if let Some(devpath) = parent.attribute_value("devpath") { - let devpath = devpath.to_string_lossy(); - format!("{id_product}_{devnum}_{devpath}") - } else { - format!("{id_product}_{devnum}") - } + let identity = if let Some(serial) = parent.attribute_value("serial") { + serial.to_string_lossy() + } else if let Some(devpath) = parent.attribute_value("devpath") { + devpath.to_string_lossy() } else { - format!("{id_product}") + parent.sysname().to_string_lossy() }; - if path.contains('.') { - warn!("dbus path for {id_product} contains `.`, removing"); - path.replace('.', "").clone_into(&mut path); - } + let path = sanitize_path_component(&format!("{id_product}_{identity}")); return Some(ObjectPath::from_str_unchecked(&path).into()); } None } +fn sanitize_path_component(value: &str) -> String { + value + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect() +} + fn dbus_path_for_dev(parent: &Device) -> Option { if let Some(filename) = filename_partial(parent) { return Some( @@ -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,59 +148,26 @@ 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 path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_anime()); - let ctrl = AniMeZbus::new(anime); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start AniMe tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } - } - // AURA LAPTOP DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) - .await + let usb_id_str = usb_id.to_str().unwrap_or_default(); + let aura_type = AuraDeviceType::from(usb_id_str); + if matches!( + aura_type, + AuraDeviceType::LaptopKeyboard2021 + | AuraDeviceType::LaptopKeyboardPre2021 + | AuraDeviceType::LaptopKeyboardTuf + | AuraDeviceType::Ally + ) { + let hid_key = device + .devnode() + .map(|path| path.to_string_lossy().into_owned()); + let hid = if HidRaw::supports_output_report(&device, 0x5d) { + HidRaw::from_device(device) + .map(|hid| Arc::new(Mutex::new(hid))) + .ok() + } else { + None + }; + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(hid, usb_id_str).await && let DeviceHandle::Aura(aura) = dev_type.clone() { let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); @@ -238,26 +183,23 @@ 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 - // interfere with the kernel's own HID driver and trigger a USB reset loop. + // A USB device can expose multiple HID interfaces (and thus multiple hidraw + // nodes). Processing more than one causes duplicate device + // initialisation which can interfere with the kernel's own HID driver + // and trigger a USB reset loop. let mut seen_usb_parents: HashSet = HashSet::new(); let mut devices: Vec = Vec::new(); @@ -281,44 +223,29 @@ impl DeviceManager { continue; } - if let Ok(Some(usb_parent)) = device.parent_with_subsystem_devtype("usb", "usb_device") + let parent_path = 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(); - if !seen_usb_parents.insert(parent_path) { + if seen_usb_parents.contains(&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?); - } - - Ok(devices) - } + Some(parent_path) + } else { + None + }; - /// Resolve the `/dev/sgN` (scsi_generic) node backing a block device. - /// - /// Walks up from the block device to its owning scsi_device and reads the - /// `scsi_generic/sgN` child. Works for whole-disk (`/dev/sda`) and - /// partition (`/dev/sda1`) nodes alike, since the scsi_device is a common - /// ancestor. Returns None if no sg node exists (e.g. the `sg` module is - /// not loaded). - fn sg_node_for_block(device: &Device) -> Option { - let mut current = device.parent(); - while let Some(d) = current { - if let Ok(entries) = std::fs::read_dir(d.syspath().join("scsi_generic")) { - for entry in entries.flatten() { - if let Some(name) = entry.file_name().to_str() { - let node = format!("/dev/{name}"); - if std::path::Path::new(&node).exists() { - return Some(node); - } - } + let mut found = Self::init_hid_devices(connection, device).await?; + if !found.is_empty() { + if let Some(parent_path) = parent_path { + seen_usb_parents.insert(parent_path); } + devices.append(&mut found); } - current = d.parent(); } - None + + Ok(devices) } async fn init_scsi( @@ -335,40 +262,9 @@ impl DeviceManager { .property_value("ID_MODEL_ID") .unwrap_or_default() .to_string_lossy(); - // SG_IO with vendor commands on the block node (/dev/sdX) - // requires CAP_SYS_RAWIO, which the hardened asusd unit drops - // (every ioctl EPERMs and is silently swallowed by write_effect). - // The scsi_generic /dev/sgN node gates access at open() via - // file permissions instead, so it works with no capabilities, - // the same path sg3_utils / OpenRGB use. - // - // On hotplug the sg node can appear just after the block node, - // so retry briefly before falling back to the block device - // (which would EPERM). At startup the node already exists, so - // the first attempt succeeds with no delay. - let mut sg_node = None; - for attempt in 0..8u8 { - if let Some(sg) = Self::sg_node_for_block(device) { - sg_node = Some(sg); - break; - } - if attempt < 7 { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } - let dev_str = match sg_node { - Some(sg) => Some(sg), - None => { - warn!( - "No /dev/sgN for SCSI device after retries; falling back to block \ - node {:?} (SG_IO will EPERM unless asusd has CAP_SYS_RAWIO)", - dev_node - ); - dev_node.as_os_str().to_str().map(|s| s.to_string()) - } - }; - if let Some(dev_str) = dev_str - && let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await + + let dev_str = dev_node.to_string_lossy(); + if let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await && let DeviceHandle::Scsi(scsi) = dev_type.clone() { let ctrl = ScsiZbus::new(scsi); @@ -388,6 +284,7 @@ impl DeviceManager { } } } + None } @@ -430,13 +327,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,13 +344,13 @@ 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 Ok(dev_type) = DeviceHandle::maybe_slash().await { if let DeviceHandle::Slash(slash) = dev_type.clone() { let path = dbus_path_for_slash(); let ctrl = SlashZbus::new(slash); @@ -505,35 +399,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 +431,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 +469,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" { @@ -628,6 +508,12 @@ impl DeviceManager { if let Some(serial) = evdev.property_value("ID_SERIAL_SHORT") { let serial = serial.to_string_lossy().to_string(); let path = dbus_path_for_scsi(&serial); + if devices.lock().await.iter().any(|d| d.dbus_path == path) { + debug!( + "SCSI hotplug add: device {path:?} already registered" + ); + return Ok(()); + } if let Some(new_devs) = Self::init_scsi(&conn_copy, &evdev, path).await { @@ -706,11 +592,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 +614,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); } @@ -755,3 +635,13 @@ impl DeviceManager { Ok(manager) } } + +#[cfg(test)] +mod tests { + use super::sanitize_path_component; + + #[test] + fn physical_identity_is_a_valid_stable_object_component() { + assert_eq!(sanitize_path_component("19b6_1-3.2:1.0"), "19b6_1_3_2_1_0"); + } +} diff --git a/asusd/src/aura_scsi/config.rs b/asusd/src/aura_scsi/config.rs index 5cf775724..4d9ce1d4f 100644 --- a/asusd/src/aura_scsi/config.rs +++ b/asusd/src/aura_scsi/config.rs @@ -56,42 +56,6 @@ impl Default for ScsiConfig { AuraMode::RainbowWave, AuraEffect::default_with_mode(AuraMode::RainbowWave), ), - ( - AuraMode::RainbowCycleBreathe, - AuraEffect::default_with_mode(AuraMode::RainbowCycleBreathe), - ), - ( - AuraMode::ChaseFade, - AuraEffect::default_with_mode(AuraMode::ChaseFade), - ), - ( - AuraMode::RainbowCycleChaseFade, - AuraEffect::default_with_mode(AuraMode::RainbowCycleChaseFade), - ), - ( - AuraMode::Chase, - AuraEffect::default_with_mode(AuraMode::Chase), - ), - ( - AuraMode::RainbowCycleChase, - AuraEffect::default_with_mode(AuraMode::RainbowCycleChase), - ), - ( - AuraMode::RainbowCycleWave, - AuraEffect::default_with_mode(AuraMode::RainbowCycleWave), - ), - ( - AuraMode::RainbowPulseChase, - AuraEffect::default_with_mode(AuraMode::RainbowPulseChase), - ), - ( - AuraMode::RandomFlicker, - AuraEffect::default_with_mode(AuraMode::RandomFlicker), - ), - ( - AuraMode::DoubleFade, - AuraEffect::default_with_mode(AuraMode::DoubleFade), - ), ]), } } diff --git a/asusd/src/aura_scsi/mod.rs b/asusd/src/aura_scsi/mod.rs index 5e77ae47c..afbb65283 100644 --- a/asusd/src/aura_scsi/mod.rs +++ b/asusd/src/aura_scsi/mod.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use config::ScsiConfig; -use rog_scsi::{AuraEffect, Device, Task}; +use rog_platform::ScsiLed; +use rog_scsi::{AuraEffect, AuraMode, Direction}; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -11,13 +12,17 @@ pub mod trait_impls; #[derive(Clone)] pub struct ScsiAura { - device: Arc>, - config: Arc>, + pub led: ScsiLed, + pub config: Arc>, } impl ScsiAura { - pub fn new(device: Arc>, config: Arc>) -> Self { - Self { device, config } + pub fn new(led: ScsiLed, config: Arc>) -> Self { + Self { led, config } + } + + pub fn led(&self) -> &ScsiLed { + &self.led } pub async fn lock_config(&self) -> MutexGuard<'_, ScsiConfig> { @@ -25,14 +30,84 @@ impl ScsiAura { } pub async fn write_effect(&self, effect: &AuraEffect) -> Result<(), RogError> { - let mut tasks: Vec = effect.into(); - for task in &mut tasks { - // Surface the ioctl errno instead of dropping it — an EPERM/EIO - // here was previously invisible, so asusd reported success while - // no SCSI traffic ever reached the device. - if let Err(e) = self.device.lock().await.perform(task) { - log::warn!("SCSI perform failed: {e}"); + Self::write_kernel_effect(&self.led, effect) + } + + fn write_kernel_effect(led: &ScsiLed, effect: &AuraEffect) -> Result<(), RogError> { + match effect.mode { + AuraMode::Off => { + led.set_effect("off").map_err(RogError::Platform)?; + } + AuraMode::Static => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + if led.dynamic().has_effects_palette() { + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + } + led.set_effect("static").map_err(RogError::Platform)?; + } + AuraMode::Breathe => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + if led.dynamic().has_effects_palette() { + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + } + if led.dynamic().has_speed() { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + } + led.set_effect("breathing").map_err(RogError::Platform)?; + } + AuraMode::Flashing => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + if led.dynamic().has_effects_palette() { + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + } + if led.dynamic().has_speed() { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + } + led.set_effect("strobe").map_err(RogError::Platform)?; + } + AuraMode::RainbowCycle => { + if led.dynamic().has_speed() { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + } + led.set_effect("spectrum_cycle") + .map_err(RogError::Platform)?; + } + AuraMode::RainbowWave => { + if led.dynamic().has_speed() { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + } + let dir = match effect.direction { + Direction::Forward => "right", + Direction::Reverse => "left", + }; + if led.dynamic().has_direction() { + led.set_direction(dir).map_err(RogError::Platform)?; + } + led.set_effect("rainbow").map_err(RogError::Platform)?; } + _ => return Err(rog_platform::error::PlatformError::NotSupported.into()), } Ok(()) } diff --git a/asusd/src/aura_scsi/trait_impls.rs b/asusd/src/aura_scsi/trait_impls.rs index 442a00143..96fd10764 100644 --- a/asusd/src/aura_scsi/trait_impls.rs +++ b/asusd/src/aura_scsi/trait_impls.rs @@ -28,9 +28,11 @@ impl ScsiZbus { .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(|_| ()) } } diff --git a/asusd/src/aura_slash/mod.rs b/asusd/src/aura_slash/mod.rs index 8996f59e7..b725eb73e 100644 --- a/asusd/src/aura_slash/mod.rs +++ b/asusd/src/aura_slash/mod.rs @@ -1,9 +1,7 @@ use std::sync::Arc; use config::SlashConfig; -use rog_platform::hid_raw::HidRaw; -use rog_platform::usb_raw::USBRaw; -use rog_slash::usb::{slash_pkt_enable, slash_pkt_init, slash_pkt_options, slash_pkt_set_mode}; +use rog_platform::slash_led::SlashLed; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -13,57 +11,32 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Slash { - hid: Option>>, - usb: Option>>, + led: SlashLed, config: Arc>, } impl Slash { - pub fn new( - hid: Option>>, - usb: Option>>, - config: Arc>, - ) -> Self { - Self { hid, usb, config } + pub fn new(led: SlashLed, config: Arc>) -> Self { + Self { led, config } } - pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { - self.config.lock().await + pub fn led(&self) -> &SlashLed { + &self.led } - pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { - usb.lock().await.write_bytes(message)?; - } - Ok(()) + pub async fn lock_config(&self) -> MutexGuard<'_, SlashConfig> { + self.config.lock().await } /// Initialise the device if required. Locks the internal config so be wary /// of deadlocks. pub async fn do_initialization(&self) -> Result<(), RogError> { - // Don't try to initialise these models as the asus drivers already did let config = self.config.lock().await; - for pkt in &slash_pkt_init(config.slash_type) { - self.write_bytes(pkt).await?; - } - self.write_bytes(&slash_pkt_enable(config.slash_type, config.enabled)) - .await?; - - // Apply config upon initialization - let option_packets = slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - ); - self.write_bytes(&option_packets).await?; - - let mode_packets = slash_pkt_set_mode(config.slash_type, config.display_mode); - // self.node.write_bytes(&mode_packets[0])?; - self.write_bytes(&mode_packets[1]).await?; + let brightness = if config.enabled { config.brightness } else { 0 }; + self.led.set_brightness(brightness)?; + self.led.set_slash_interval(config.display_interval)?; + self.led.set_slash_mode(&config.display_mode.to_string())?; Ok(()) } } diff --git a/asusd/src/aura_slash/trait_impls.rs b/asusd/src/aura_slash/trait_impls.rs index 70aa1ad36..0ad65ab26 100644 --- a/asusd/src/aura_slash/trait_impls.rs +++ b/asusd/src/aura_slash/trait_impls.rs @@ -1,10 +1,5 @@ use config_traits::StdConfig; use log::{debug, error, warn}; -use rog_slash::usb::{ - slash_pkt_battery_saver, slash_pkt_boot, slash_pkt_enable, slash_pkt_lid_closed, - slash_pkt_low_battery, slash_pkt_options, slash_pkt_save, slash_pkt_set_mode, - slash_pkt_shutdown, slash_pkt_sleep, -}; use rog_slash::{DeviceState, SlashMode}; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; @@ -13,6 +8,15 @@ use super::Slash; use crate::Reloadable; use crate::error::RogError; +const SLASH_POWER_UNSUPPORTED: &str = concat!( + "Kernel Slash LED has no power_states; ", + "boot/sleep/shutdown/battery/lid persistence is not supported" +); + +fn slash_power_not_supported() -> zbus::fdo::Error { + zbus::fdo::Error::NotSupported(SLASH_POWER_UNSUPPORTED.into()) +} + #[derive(Clone)] pub struct SlashZbus(Slash); @@ -26,7 +30,6 @@ impl SlashZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -34,9 +37,11 @@ impl SlashZbus { .object_server() .at(path.clone(), self) .await - .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) - .ok(); - Ok(()) + .map_err(|e| { + error!("Couldn't add server at path: {path}, {e:?}"); + RogError::from(e) + }) + .map(|_| ()) } } @@ -58,25 +63,11 @@ impl SlashZbus { } else { config.brightness }; - self.0 - .write_bytes(&slash_pkt_enable(config.slash_type, enabled)) - .await - .map_err(|err| { - warn!("ctrl_slash::enable {}", err); - }) - .ok(); - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - enabled, - brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + let b = if enabled { brightness } else { 0 }; + if let Err(err) = self.0.led().set_brightness(b) { + warn!("ctrl_slash::set_enabled via sysfs: {err}"); + } config.enabled = enabled; config.brightness = brightness; @@ -95,18 +86,10 @@ impl SlashZbus { async fn set_brightness(&self, brightness: u8) { let mut config = self.0.lock_config().await; let enabled = brightness > 0; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - enabled, - brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + if let Err(err) = self.0.led().set_brightness(brightness) { + warn!("ctrl_slash::set_brightness via sysfs: {err}"); + } config.enabled = enabled; config.brightness = brightness; @@ -123,15 +106,10 @@ impl SlashZbus { #[zbus(property)] async fn set_interval(&self, interval: u8) { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, config.enabled, config.brightness, interval, - )) - .await - .map_err(|err| { - warn!("ctrl_slash::set_options {}", err); - }) - .ok(); + + if let Err(err) = self.0.led().set_slash_interval(interval) { + warn!("ctrl_slash::set_interval via sysfs: {err}"); + } config.display_interval = interval; config.write(); @@ -143,7 +121,7 @@ impl SlashZbus { Ok(config.display_mode as u8) } - /// Set interval between slash animations (0-255) + /// Set animation mode #[zbus(property)] async fn set_mode(&self, mode: u8) -> zbus::Result<()> { let mode = SlashMode::try_from(mode).map_err(|err| { @@ -151,12 +129,12 @@ impl SlashZbus { })?; let mut config = self.0.lock_config().await; - let command_packets = slash_pkt_set_mode(config.slash_type, mode); - // self.node.write_bytes(&command_packets[0])?; - self.0.write_bytes(&command_packets[1]).await?; self.0 - .write_bytes(&slash_pkt_save(config.slash_type)) - .await?; + .led() + .set_slash_mode(&mode.to_string()) + .map_err(|err| { + zbus::fdo::Error::Failed(format!("ctrl_slash::set_mode sysfs: {err}")) + })?; config.display_mode = mode; config.write(); @@ -164,7 +142,6 @@ impl SlashZbus { } /// Get the device state as stored by asusd - // #[zbus(property)] async fn device_state(&self) -> DeviceState { let config = self.0.lock_config().await; DeviceState::from(&*config) @@ -177,14 +154,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_boot(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_boot(config.slash_type, enable)) - .await?; - config.show_on_boot = enable; - config.write(); - Ok(()) + async fn set_show_on_boot(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -194,14 +165,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_sleep(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_sleep(config.slash_type, enable)) - .await?; - config.show_on_sleep = enable; - config.write(); - Ok(()) + async fn set_show_on_sleep(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -211,14 +176,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_shutdown(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_shutdown(config.slash_type, enable)) - .await?; - config.show_on_shutdown = enable; - config.write(); - Ok(()) + async fn set_show_on_shutdown(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -228,14 +187,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_battery(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_battery_saver(config.slash_type, enable)) - .await?; - config.show_on_battery = enable; - config.write(); - Ok(()) + async fn set_show_on_battery(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -245,14 +198,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_battery_warning(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_low_battery(config.slash_type, enable)) - .await?; - config.show_battery_warning = enable; - config.write(); - Ok(()) + async fn set_show_battery_warning(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } #[zbus(property)] @@ -262,17 +209,8 @@ impl SlashZbus { } #[zbus(property)] - async fn set_show_on_lid_closed(&self, enable: bool) -> zbus::Result<()> { - let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_lid_closed(config.slash_type, enable)) - .await?; - self.0 - .write_bytes(&slash_pkt_save(config.slash_type)) - .await?; - config.show_on_lid_closed = enable; - config.write(); - Ok(()) + async fn set_show_on_lid_closed(&self, _enable: bool) -> zbus::Result<()> { + Err(slash_power_not_supported().into()) } } @@ -280,40 +218,13 @@ impl Reloadable for SlashZbus { async fn reload(&mut self) -> Result<(), RogError> { debug!("reloading slash settings"); let config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_options( - config.slash_type, - config.enabled, - config.brightness, - config.display_interval, - )) - .await - .map_err(|err| { - warn!("set_options {}", err); - }) - .ok(); - - macro_rules! write_bytes_with_warning { - ($packet_fn:expr, $cfg:ident, $warn_msg:expr) => { - self.0 - .write_bytes(&$packet_fn(config.slash_type, config.$cfg)) - .await - .map_err(|err| { - warn!("{} {}", $warn_msg, err); - }) - .ok(); - }; - } - write_bytes_with_warning!(slash_pkt_boot, show_on_boot, "show_on_boot"); - write_bytes_with_warning!(slash_pkt_sleep, show_on_sleep, "show_on_sleep"); - write_bytes_with_warning!(slash_pkt_shutdown, show_on_shutdown, "show_on_shutdown"); - write_bytes_with_warning!(slash_pkt_battery_saver, show_on_battery, "show_on_battery"); - write_bytes_with_warning!( - slash_pkt_low_battery, - show_battery_warning, - "show_battery_warning" - ); + let brightness = if config.enabled { config.brightness } else { 0 }; + self.0.led().set_brightness(brightness)?; + self.0.led().set_slash_interval(config.display_interval)?; + self.0 + .led() + .set_slash_mode(&config.display_mode.to_string())?; Ok(()) } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index c31de89a0..37db642ca 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use config_traits::{StdConfig, StdConfigLoad}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use rog_anime::AnimeType; use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; @@ -9,9 +9,9 @@ use rog_aura::AuraDeviceType; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; -use rog_scsi::{ScsiType, open_device}; +use rog_platform::{DynamicLed, ScsiLed, SlashLed}; +use rog_scsi::ScsiType; use rog_slash::SlashType; -use rog_slash::error::SlashError; use tokio::sync::Mutex; use crate::aura_anime::AniMe; @@ -24,16 +24,6 @@ use crate::aura_slash::Slash; use crate::aura_slash::config::SlashConfig; 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, - None, -} - #[derive(Clone)] pub enum DeviceHandle { Aura(Aura), @@ -41,93 +31,29 @@ 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. - pub async fn new_slash_hid( - device: Arc>, - prod_id: &str, - ) -> Result { - debug!("Testing for HIDRAW Slash"); - let slash_type = SlashType::from_dmi(); - if matches!(slash_type, SlashType::Unsupported) - || slash_type - .prod_id_str() - .to_lowercase() - .trim_start_matches("0x") - != prod_id - { - log::info!("Unknown or invalid slash: {prod_id:?}, skipping"); - return Err(RogError::NotFound("No slash device".to_string())); - } - info!("Found slash type {slash_type:?}: {prod_id}"); - + /// Try Slash sysfs LED. Presence of `asus::slash` is the capability gate; + /// board DMI lists are not used. + pub async fn maybe_slash() -> Result { + debug!("Testing for Slash"); + let led = SlashLed::new().map_err(|e| { + warn!("No Slash sysfs LED found: {e}"); + RogError::NotFound("No slash device found".to_string()) + })?; + + info!("Found Slash sysfs LED at {:?}", led.path()); let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new(Some(device), None, Arc::new(Mutex::new(config))); + config.slash_type = SlashType::Unsupported; + let slash = Slash::new(led, Arc::new(Mutex::new(config))); slash.do_initialization().await?; Ok(Self::Slash(slash)) } - /// Try Slash USB. If one exists it is initialsed and returned. - pub async fn new_slash_usb() -> Result { - debug!("Testing for USB Slash"); - let slash_type = SlashType::from_dmi(); - if matches!(slash_type, SlashType::Unsupported) { - return Err(RogError::Slash(SlashError::NoDevice)); - } - - if let Ok(usb) = USBRaw::new(slash_type.prod_id()) { - info!("Found Slash USB {slash_type:?}"); - - let mut config = SlashConfig::new().load(); - config.slash_type = slash_type; - let slash = Slash::new( - None, - Some(Arc::new(Mutex::new(usb))), - Arc::new(Mutex::new(config)), - ); - slash.do_initialization().await?; - Ok(Self::Slash(slash)) - } else { - Err(RogError::NotFound("No slash device found".to_string())) - } - } - - /// Try AniMe Matrix HID. If one exists it is initialsed and returned. - pub async fn maybe_anime_hid( - _device: Arc>, - _prod_id: &str, - ) -> Result { - // TODO: can't use HIDRAW for anime at the moment - Err(RogError::NotFound( - "Can't use anime over hidraw yet. Skip.".to_string(), - )) - - // debug!("Testing for HIDRAW AniMe"); - // let anime_type = AnimeType::from_dmi(); - // dbg!(prod_id); - // if matches!(anime_type, AnimeType::Unsupported) || prod_id != "193b" - // { log::info!("Unknown or invalid AniMe: {prod_id:?}, - // skipping"); return Err(RogError::NotFound("No - // anime-matrix device".to_string())); } - // info!("Found AniMe Matrix HIDRAW {anime_type:?}: {prod_id}"); - - // let mut config = AniMeConfig::new().load(); - // config.anime_type = anime_type; - // let mut anime = AniMe::new(Some(device), None, - // Arc::new(Mutex::new(config))); anime.do_initialization(). - // await?; Ok(Self::AniMe(anime)) - } - pub async fn maybe_anime_usb() -> Result { debug!("Testing for USB AniMe"); let anime_type = get_anime_type(); @@ -142,7 +68,6 @@ impl DeviceHandle { let mut config = AniMeConfig::new().load(); config.anime_type = anime_type; let mut anime = AniMe::new( - None, Some(Arc::new(Mutex::new(usb))), Arc::new(Mutex::new(config)), ); @@ -156,24 +81,51 @@ impl DeviceHandle { } pub async fn maybe_scsi(dev_node: &str, prod_id: &str) -> Result { - debug!("Testing for SCSI"); - let prod_id = ScsiType::from(prod_id); - if prod_id == ScsiType::Unsupported { - log::info!("Unknown or invalid SCSI: {prod_id:?}, skipping"); + let scsi_type = ScsiType::from(prod_id); + if scsi_type == ScsiType::Unsupported { + log::info!("Unknown or invalid SCSI: {scsi_type:?}, skipping"); return Err(RogError::NotFound("No SCSI device".to_string())); } - info!("Found SCSI device {prod_id:?} on {dev_node}"); + + let mut last_error = None; + let mut led = None; + for _ in 0..20 { + match ScsiLed::find_for_dev(dev_node) { + Ok(found) => { + led = Some(found); + break; + } + Err(err @ rog_platform::error::PlatformError::MissingFunction(_)) => { + last_error = Some(err); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Err(err) => return Err(err.into()), + } + } + let led = led.ok_or_else(|| { + let err = last_error + .map(|err| err.to_string()) + .unwrap_or_else(|| "unknown discovery error".into()); + log::warn!("No exact SCSI Dynamic Lighting device found for {dev_node}: {err}"); + RogError::NotFound(format!( + "Dynamic Lighting registration timed out for {dev_node}" + )) + })?; + + info!( + "Found SCSI Dynamic Lighting device {scsi_type:?} on {:?}", + led.path() + ); let mut config = ScsiConfig::new().load(); config.dev_type = AuraDeviceType::ScsiExtDisk; - let dev = Arc::new(Mutex::new(open_device(dev_node)?)); - let scsi = ScsiAura::new(dev, Arc::new(Mutex::new(config))); + let scsi = ScsiAura::new(led, Arc::new(Mutex::new(config))); scsi.do_initialization().await?; Ok(Self::Scsi(scsi)) } pub async fn maybe_laptop_aura( - device: Option>>, + hid: Option>>, prod_id: &str, ) -> Result { debug!("Testing for laptop aura"); @@ -197,11 +149,55 @@ impl DeviceHandle { Some(Arc::new(Mutex::new(k))) }); + // Check for Dynamic Lighting interface + let (dynamic_global, dynamic_kbd, dynamic_lightbar) = { + let global = DynamicLed::find("aura:global") + .map(|g| { + info!("Dynamic Lighting global aggregate detected: aura:global"); + Arc::new(Mutex::new(g)) + }) + .ok(); + let kbd = DynamicLed::find("aura:keyboard") + .or_else(|_| DynamicLed::find("asus::kbd_backlight")) + .map(|k| { + info!("Dynamic Lighting keyboard detected: {}", k.name()); + Arc::new(Mutex::new(k)) + }) + .ok(); + let lb = DynamicLed::find("aura:lightbar") + .map(|l| { + info!("Dynamic Lighting lightbar detected: aura:lightbar"); + Arc::new(Mutex::new(l)) + }) + .ok(); + (global, kbd, lb) + }; + + let dynamic_available = + dynamic_global.is_some() || dynamic_kbd.is_some() || dynamic_lightbar.is_some(); + let fallback_available = if matches!(aura_type, AuraDeviceType::LaptopKeyboardTuf) { + backlight.is_some() + } else { + hid.is_some() + }; + if !dynamic_available && !fallback_available { + debug!("Neither valid Dynamic Lighting nor device-specific fallback detected"); + return Err(RogError::NotFound( + "No Dynamic Lighting or legacy Aura control path found".to_string(), + )); + } + // Load saved mode, colours, brightness, power from disk; apply on reload let mut config = AuraConfig::load_and_update_config(prod_id); config.led_type = aura_type; + let use_hid = !dynamic_available; let aura = Aura { - hid: device, + dynamic_global, + dynamic_kbd, + dynamic_lightbar, + // One device has exactly one owner: valid Dynamic Lighting nodes win, + // otherwise retain the matching hidraw handle for released kernels. + hid: use_hid.then_some(hid).flatten(), backlight, config: Arc::new(Mutex::new(config)), }; diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4340c9385..279225281 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -24,6 +24,7 @@ # Usage - [asusctl](usage/asusctl.md) +- [Dynamic Lighting and legacy fallback](usage/dynamic-lighting.md) # FAQ diff --git a/docs/usage/dynamic-lighting.md b/docs/usage/dynamic-lighting.md new file mode 100644 index 000000000..362204869 --- /dev/null +++ b/docs/usage/dynamic-lighting.md @@ -0,0 +1,55 @@ +# Dynamic Lighting and legacy fallback + +asusd prefers the Linux LED Dynamic Lighting ABI when a device exposes a valid +node below `/sys/class/leds`. A valid node must provide the generic `effect`, +`effect_index`, `zone_type`, and `led_count` attributes. Optional speed, +direction, palette, power-state, and direct-buffer controls are used only when +the node advertises them. + +On supported ROG laptop keyboards, asusd retains the matching `hidraw` output +report as a capability-based fallback when no valid Dynamic Lighting node is +available. The two paths are mutually exclusive for a physical device: +Dynamic Lighting always wins, and hidraw is not used to retry an effect rejected +by the kernel. This preserves operation on released kernels while avoiding two +owners sending commands to the same controller. + +## Topology (`aura_mode`) + +The ASUS-only `aura_mode` attribute selects kernel topology; it is not part of +the generic Dynamic Lighting ABI. + +| Mode | Writable nodes | Notes | +|------|----------------|-------| +| `auto` | same as `split` | Kernel default resolution | +| `split` | `aura:keyboard`, `aura:lightbar` | Independent colours; `aura:global` returns `-EBUSY` | +| `unified` | `aura:global` | Single effect for all zones; split nodes return `-EBUSY` | + +When a chassis lightbar is present, asusd sets `split` during initialization so +keyboard and lightbar are independently controllable. Callers that want one +shared effect should set `unified` explicitly. + +Kernel direct RGB may use HID LampArray (Usage Page `0x59`) when Aura `0xBC` +cannot drive the lightbar independently; firmware animations stay on Aura +`0xb3`. That backend choice is invisible to the sysfs ABI. + +## D-Bus zones + +The D-Bus API still describes historical keyboard and left/right lightbar +subzones. Under Dynamic Lighting those are not advertised +(`supported_basic_zones` is empty). Incoming legacy zone values are accepted +only as aliases: Key1–4 map to the keyboard node, BarLeft/BarRight to the +lightbar, and `None` fans out under split or uses `aura:global` under unified. + +## Other devices + +ROG NVMe enclosure lighting requires the kernel ASUS Aura SCSI Dynamic Lighting +driver. asusd matches an enclosure's block-device ancestry to its exact LED +node and briefly retries while that node is being registered. It never falls +back to the first enclosure. The old public `rog_scsi` SG_IO API was removed +intentionally: vendor commands are kernel-owned, and applications must use the +Dynamic Lighting sysfs ABI. + +The optional generic `frame` attribute is not currently used by asusd. Direct +streaming uses `direct_buffer` and requires exactly `led_count * 3` RGB bytes. +Hardware-specific behavior still depends on the kernel driver reporting correct +topology, LED count, and optional attributes. diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..f8d73efe7 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,24 @@ impl From for u8 { } } } + +impl Speed { + pub const fn to_dynamic_speed(&self) -> u32 { + match self { + Self::Low => 0, + Self::Med => 1, + Self::High => 2, + } + } + + pub const fn from_dynamic_speed(val: u32) -> Self { + match val { + 0 => Self::Low, + 2 => Self::High, + _ => Self::Med, + } + } +} /// Used for Rainbow mode. /// /// Enum corresponds to the required integer value @@ -248,6 +306,26 @@ impl From for i32 { } } +impl Direction { + pub const fn to_dynamic_direction_str(&self) -> &'static str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Up => "up", + Self::Down => "down", + } + } + + pub fn from_dynamic_direction_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "left" => Self::Left, + "up" => Self::Up, + "down" => Self::Down, + _ => Self::Right, + } + } +} + /// Enum of modes that convert to the actual number required by a USB HID packet #[cfg_attr( feature = "dbus", @@ -359,6 +437,32 @@ impl From for AuraModeNum { } } +impl AuraModeNum { + /// Return the corresponding Dynamic Lighting effect name, if available. + pub const fn to_dynamic_effect_str(&self) -> Option<&'static str> { + match self { + Self::Static => Some("static"), + Self::Breathe => Some("breathing"), + Self::RainbowCycle => Some("spectrum_cycle"), + Self::RainbowWave => Some("rainbow"), + Self::Pulse | Self::Flash => Some("strobe"), + _ => None, + } + } + + /// Parse a Dynamic Lighting effect name into an `AuraModeNum`. + pub fn from_dynamic_effect_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "static" => Some(Self::Static), + "breathing" => Some(Self::Breathe), + "spectrum_cycle" => Some(Self::RainbowCycle), + "rainbow" => Some(Self::RainbowWave), + "strobe" => Some(Self::Pulse), + _ => None, + } + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +508,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 +573,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 +653,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 +822,88 @@ mod tests { capture[..9] ); } + + #[test] + fn test_dynamic_lighting_conversions() { + assert_eq!(AuraModeNum::Static.to_dynamic_effect_str(), Some("static")); + assert_eq!( + AuraModeNum::Breathe.to_dynamic_effect_str(), + Some("breathing") + ); + assert_eq!( + AuraModeNum::RainbowCycle.to_dynamic_effect_str(), + Some("spectrum_cycle") + ); + assert_eq!( + AuraModeNum::RainbowWave.to_dynamic_effect_str(), + Some("rainbow") + ); + assert_eq!(AuraModeNum::Pulse.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), None); + + assert_eq!( + AuraModeNum::from_dynamic_effect_str("static"), + Some(AuraModeNum::Static) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("breathing"), + Some(AuraModeNum::Breathe) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("spectrum_cycle"), + Some(AuraModeNum::RainbowCycle) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("rainbow"), + Some(AuraModeNum::RainbowWave) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("strobe"), + Some(AuraModeNum::Pulse) + ); + assert_eq!(AuraModeNum::from_dynamic_effect_str("unknown"), None); + + assert_eq!(Speed::Low.to_dynamic_speed(), 0); + assert_eq!(Speed::Med.to_dynamic_speed(), 1); + assert_eq!(Speed::High.to_dynamic_speed(), 2); + assert_eq!(Speed::from_dynamic_speed(0), Speed::Low); + assert_eq!(Speed::from_dynamic_speed(1), Speed::Med); + assert_eq!(Speed::from_dynamic_speed(2), Speed::High); + + assert_eq!(Direction::Right.to_dynamic_direction_str(), "right"); + assert_eq!(Direction::Left.to_dynamic_direction_str(), "left"); + assert_eq!(Direction::Up.to_dynamic_direction_str(), "up"); + assert_eq!(Direction::Down.to_dynamic_direction_str(), "down"); + assert_eq!( + Direction::from_dynamic_direction_str("left"), + Direction::Left + ); + assert_eq!( + Direction::from_dynamic_direction_str("right"), + Direction::Right + ); + + let effect = AuraEffect { + colour1: Colour { + r: 0xff, + g: 0x10, + b: 0x20, + }, + colour2: Colour { + r: 0x00, + g: 0x30, + b: 0x40, + }, + ..Default::default() + }; + let palette = effect.to_dynamic_palette(); + assert_eq!( + palette, + vec![ + (0xff, 0x10, 0x20), + (0x00, 0x30, 0x40) + ] + ); + } } diff --git a/rog-platform/src/dynamic_led.rs b/rog-platform/src/dynamic_led.rs new file mode 100644 index 000000000..a60850874 --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,329 @@ +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +/// Dynamic Lighting class device under `/sys/class/leds/`. +/// +/// Wraps a kernel `led-class-dynamic` sysfs node exposing effects, palette, +/// speed, direction, power states, direct buffer streaming, and standard +/// brightness attributes. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct DynamicLed { + path: PathBuf, +} + +impl DynamicLed { + attr_string!("effect", path); + + attr_string!("effect_index", path); + + attr_string!("direction", path); + + attr_string!("direction_index", path); + + attr_string!("effects_palette", path); + + attr_string!("speed_range", path); + + attr_string!("zone_type", path); + + attr_string!("matrix_dimensions", path); + + attr_string!("power_states", path); + + attr_string!("power_states_index", path); + + attr_num!("speed", path, u32); + + attr_num!("max_palette_entries", path, u32); + + attr_num!("led_count", path, u32); + + attr_num!("brightness", path, u8); + + attr_num!("max_brightness", path, u8); + + /// Create a new `DynamicLed` by matching the exact sysfs name (e.g. + /// `"aura:keyboard"`). + pub fn new(name: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("DynamicLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("DynamicLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("DynamicLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname == name { + let syspath = device.syspath(); + if Self::is_dynamic_node(syspath) { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: syspath.to_path_buf(), + }); + } + } + } + + Err(PlatformError::MissingFunction(format!( + "DynamicLed::new(): no dynamic LED named '{name}' found" + ))) + } + + /// Helper to find a dynamic LED by name. + pub fn find(name: &str) -> Result { + Self::new(name) + } + + /// Return the LED name (e.g. "aura:keyboard" or "asus::kbd_backlight"). + pub fn name(&self) -> &str { + self.path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + } + + /// Check if a dynamic LED is present on the system. + pub fn is_available(name: &str) -> bool { + Self::find(name).is_ok() + } + + /// Return the sysfs path. + pub fn path(&self) -> &Path { + &self.path + } + + pub fn from_syspath(path: PathBuf) -> Result { + if !Self::is_dynamic_node(&path) { + return Err(PlatformError::MissingFunction(format!( + "{} is not a Dynamic Lighting LED node", + path.display() + ))); + } + Ok(Self { path }) + } + + fn is_dynamic_node(path: &Path) -> bool { + [ + "effect", "effect_index", "zone_type", "led_count", + ] + .iter() + .all(|attr| path.join(attr).exists()) + } + + /// Read and parse space-separated list of supported effects from + /// `effect_index`. + pub fn get_supported_effects_list(&self) -> Result> { + let raw = self.get_effect_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Check if a given effect mode string is supported by the kernel driver. + pub fn is_effect_supported(&self, effect: &str) -> bool { + self.supports_effect(effect).unwrap_or(false) + } + + /// Check effect support while preserving sysfs read errors. + pub fn supports_effect(&self, effect: &str) -> Result { + Ok(self + .get_supported_effects_list()? + .iter() + .any(|candidate| candidate == effect)) + } + + /// Read and parse space-separated list of supported directions from + /// `direction_index`. + pub fn get_supported_directions_list(&self) -> Result> { + let raw = self.get_direction_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Set speed after validating the optional advertised inclusive range. + pub fn set_supported_speed(&self, speed: u32) -> Result<()> { + if !self.has_speed() { + return Err(PlatformError::AttrNotFound("speed".into())); + } + if self.has_speed_range() { + let (min, max) = parse_speed_range(&self.get_speed_range()?)?; + if !(min..=max).contains(&speed) { + return Err(PlatformError::InvalidValue); + } + } + self.set_speed(speed) + } + + /// Set direction after validating the optional advertised values. + pub fn set_supported_direction(&self, direction: &str) -> Result<()> { + if !self.has_direction() { + return Err(PlatformError::AttrNotFound("direction".into())); + } + if self.has_direction_index() + && !self + .get_supported_directions_list()? + .iter() + .any(|candidate| candidate == direction) + { + return Err(PlatformError::InvalidValue); + } + self.set_direction(direction) + } + + /// Write one complete RGB frame to the `direct_buffer` binary attribute. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + let led_count = self.get_led_count()? as usize; + validate_direct_len(led_count, data.len())?; + + let direct_path = self.path.join("direct_buffer"); + let mut file = OpenOptions::new() + .write(true) + .open(&direct_path) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e))?; + file.write_all(data) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e)) + } + + /// Write palette colors as formatted `"#RRGGBB #RRGGBB ..."` string to + /// `effects_palette`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + if !self.has_effects_palette() { + return Err(PlatformError::AttrNotFound("effects_palette".into())); + } + if self.has_max_palette_entries() && colors.len() > self.get_max_palette_entries()? as usize + { + return Err(PlatformError::InvalidValue); + } + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + self.set_effects_palette(&palette_str) + } + + /// Whether this ASUS node exposes the non-generic topology selector. + pub fn has_asus_aura_mode(&self) -> bool { + self.name().starts_with("aura:") && self.path.join("aura_mode").exists() + } + + /// Read the active ASUS topology mode. + pub fn get_asus_aura_mode(&self) -> Result { + parse_active_index( + &std::fs::read_to_string(self.path.join("aura_mode")).map_err(|e| { + PlatformError::IoPath(self.path.join("aura_mode").display().to_string(), e) + })?, + ) + } + + /// Set the ASUS topology mode (`auto`, `unified`, or `split`). + pub fn set_asus_aura_mode(&self, mode: &str) -> Result<()> { + if !matches!(mode, "auto" | "unified" | "split") { + return Err(PlatformError::InvalidValue); + } + let path = self.path.join("aura_mode"); + std::fs::write(&path, mode) + .map_err(|e| PlatformError::IoPath(path.display().to_string(), e)) + } +} + +fn validate_direct_len(led_count: usize, data_len: usize) -> Result<()> { + let expected = led_count + .checked_mul(3) + .ok_or(PlatformError::InvalidValue)?; + if data_len == expected { + Ok(()) + } else { + Err(PlatformError::InvalidValue) + } +} + +fn parse_active_index(raw: &str) -> Result { + raw.split_whitespace() + .find_map(|word| word.strip_prefix('[')?.strip_suffix(']')) + .map(str::to_owned) + .ok_or(PlatformError::InvalidValue) +} + +fn parse_speed_range(raw: &str) -> Result<(u32, u32)> { + let values: Vec<_> = raw + .split(|c: char| !c.is_ascii_digit()) + .filter(|value| !value.is_empty()) + .map(str::parse::) + .collect::>() + .map_err(|_| PlatformError::ParseNum)?; + match values.as_slice() { + [min, max] if min <= max => Ok((*min, *max)), + _ => Err(PlatformError::InvalidValue), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + #[test] + fn test_parse_active_aura_mode() { + let sample = "[auto] unified split\n"; + assert_eq!(super::parse_active_index(sample).unwrap(), "auto"); + + let sample2 = "auto [unified] split\n"; + assert_eq!(super::parse_active_index(sample2).unwrap(), "unified"); + assert!(super::parse_active_index("auto unified split").is_err()); + } + + #[test] + fn test_palette_colors_formatting() { + let colors = [ + (255, 0, 128), + (0, 255, 64), + ]; + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + assert_eq!(palette_str, "#ff0080 #00ff40"); + } + + #[test] + fn validates_exact_direct_frame_size() { + assert!(super::validate_direct_len(4, 12).is_ok()); + assert!(super::validate_direct_len(4, 11).is_err()); + assert!(super::validate_direct_len(usize::MAX, 0).is_err()); + } + + #[test] + fn parses_bounded_speed_range() { + assert_eq!(super::parse_speed_range("0 2\n").unwrap(), (0, 2)); + assert_eq!(super::parse_speed_range("[1-4]").unwrap(), (1, 4)); + assert!(super::parse_speed_range("fast slow").is_err()); + assert!(super::parse_speed_range("4 1").is_err()); + } + + #[test] + fn validates_mandatory_generic_attributes() { + let path = + std::env::temp_dir().join(format!("asusctl-dynamic-led-test-{}", std::process::id())); + let _ = fs::remove_dir_all(&path); + fs::create_dir(&path).expect("temporary test directory must be creatable"); + for attr in [ + "effect", "effect_index", "zone_type", "led_count", + ] { + fs::write(path.join(attr), b"").expect("temporary attribute must be writable"); + } + assert!(super::DynamicLed::is_dynamic_node(&path)); + fs::remove_file(path.join("zone_type")).expect("test attribute must be removable"); + assert!(!super::DynamicLed::is_dynamic_node(&path)); + fs::remove_dir_all(path).expect("temporary test directory must be removable"); + } +} diff --git a/rog-platform/src/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..f37e3418d 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,13 +13,18 @@ pub mod keyboard_led; pub(crate) mod macros; pub mod platform; pub mod power; +pub mod scsi_led; +pub mod slash_led; pub mod usb_raw; use std::path::Path; +pub use dynamic_led::DynamicLed; use error::{PlatformError, Result}; use log::warn; use platform::PlatformProfile; +pub use scsi_led::ScsiLed; +pub use slash_led::SlashLed; use udev::Device; 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/scsi_led.rs b/rog-platform/src/scsi_led.rs new file mode 100644 index 000000000..7285f728d --- /dev/null +++ b/rog-platform/src/scsi_led.rs @@ -0,0 +1,198 @@ +use std::path::Path; + +use log::{info, warn}; + +use crate::dynamic_led::DynamicLed; +use crate::error::{PlatformError, Result}; + +/// Generic control interface for ASUS Aura SCSI-attached lighting devices +/// backed by the kernel `leds-asus-aura-scsi` Dynamic Lighting driver. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct ScsiLed { + dynamic: DynamicLed, +} + +impl ScsiLed { + /// Discover the first available ASUS SCSI dynamic lighting LED node. + pub fn new() -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("ScsiLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("ScsiLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("ScsiLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname.contains("asus-scsi") + || sysname.contains("asus-aura-scsi") + || sysname.contains("asus-arion") + { + info!( + "Found ASUS SCSI Dynamic Lighting LED device at {:?}", + sysname + ); + let dynamic = DynamicLed::find(&sysname)?; + return Ok(Self { dynamic }); + } + } + + Err(PlatformError::MissingFunction( + "ScsiLed::new(): no asus scsi dynamic LED device found".into(), + )) + } + + /// Find the LED whose sysfs ancestry belongs to this exact SCSI device. + pub fn find_for_block(device: &udev::Device) -> Result { + let mut current = device.parent(); + let mut scsi_path = None; + while let Some(d) = current { + if let Some(sub) = d.subsystem() + && sub == "scsi" + { + let s = d.sysname().to_string_lossy(); + if s.contains(':') { + scsi_path = Some(d.syspath().to_path_buf()); + break; + } + } + current = d.parent(); + } + + let scsi_path = scsi_path.ok_or_else(|| { + PlatformError::MissingFunction(format!( + "No SCSI parent found for {}", + device.syspath().display() + )) + })?; + let mut enumerator = udev::Enumerator::new() + .map_err(|err| PlatformError::Udev("enumerator failed".into(), err))?; + enumerator + .match_subsystem("leds") + .map_err(|err| PlatformError::Udev("match_subsystem failed".into(), err))?; + + for dev in enumerator + .scan_devices() + .map_err(|err| PlatformError::Udev("scan_devices failed".into(), err))? + { + if belongs_to_scsi(dev.syspath(), &scsi_path) { + let dynamic = DynamicLed::from_syspath(dev.syspath().to_path_buf())?; + info!( + "Found exact SCSI Dynamic Lighting LED {:?} for {}", + dev.sysname(), + scsi_path.display() + ); + return Ok(Self { dynamic }); + } + } + + Err(PlatformError::MissingFunction(format!( + "Dynamic Lighting LED for {} is not registered yet", + scsi_path.display() + ))) + } + + /// Find a `ScsiLed` for a specific `/dev/sdX` or `/dev/sgN` path. + pub fn find_for_dev(dev_node: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|e| { + warn!("ScsiLed udev enumerator failed: {e}"); + PlatformError::Udev("enumerator failed".into(), e) + })?; + enumerator.match_subsystem("block").map_err(|e| { + warn!("ScsiLed match_subsystem failed: {e}"); + PlatformError::Udev("match block failed".into(), e) + })?; + + for dev in enumerator.scan_devices().map_err(|e| { + warn!("ScsiLed scan_devices failed: {e}"); + PlatformError::Udev("scan failed".into(), e) + })? { + if let Some(node) = dev.devnode() + && node.to_string_lossy() == dev_node + { + return Self::find_for_block(&dev); + } + } + + Err(PlatformError::MissingFunction(format!( + "No block device found for {dev_node}" + ))) + } + + /// Check if an ASUS SCSI Dynamic Lighting LED is available on the system. + pub fn is_available() -> bool { + Self::new().is_ok() + } + + /// Return reference to inner `DynamicLed`. + pub fn dynamic(&self) -> &DynamicLed { + &self.dynamic + } + + /// Return path to the sysfs node. + pub fn path(&self) -> &Path { + self.dynamic.path() + } + + /// Set animation effect string. + pub fn set_effect(&self, effect: &str) -> Result<()> { + self.dynamic.set_effect(effect) + } + + /// Set effect animation speed. + pub fn set_speed(&self, speed: u32) -> Result<()> { + self.dynamic.set_supported_speed(speed) + } + + /// Set effect animation direction ("right" or "left"). + pub fn set_direction(&self, direction: &str) -> Result<()> { + self.dynamic.set_supported_direction(direction) + } + + /// Set palette colors formatted as `(r, g, b)`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + self.dynamic.set_palette_colors(colors) + } + + /// Write raw RGB bytes to the direct buffer. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + self.dynamic.write_direct(data) + } + + /// Set brightness (0..=255). + pub fn set_brightness(&self, brightness: u8) -> Result<()> { + self.dynamic.set_brightness(brightness) + } + + /// Get brightness (0..=255). + pub fn get_brightness(&self) -> Result { + self.dynamic.get_brightness() + } +} + +fn belongs_to_scsi(led_path: &Path, scsi_path: &Path) -> bool { + led_path.starts_with(scsi_path) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + #[test] + fn matches_only_exact_scsi_ancestry() { + let scsi = Path::new("/sys/devices/usb/2:0:0:0"); + assert!(super::belongs_to_scsi( + Path::new("/sys/devices/usb/2:0:0:0/leds/asus-aura-scsi"), + scsi + )); + assert!(!super::belongs_to_scsi( + Path::new("/sys/devices/usb/2:0:0:01/leds/asus-aura-scsi"), + scsi + )); + } +} diff --git a/rog-platform/src/slash_led.rs b/rog-platform/src/slash_led.rs new file mode 100644 index 000000000..98fb8aeba --- /dev/null +++ b/rog-platform/src/slash_led.rs @@ -0,0 +1,65 @@ +use std::path::{Path, PathBuf}; + +use log::{info, warn}; + +use crate::error::{PlatformError, Result}; +use crate::{attr_num, attr_string, to_device}; + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Clone)] +pub struct SlashLed { + path: PathBuf, +} + +impl SlashLed { + attr_num!("brightness", path, u8); + attr_num!("max_brightness", path, u8); + + attr_string!("slash_mode", path); + attr_string!("slash_mode_index", path); + attr_num!("slash_interval", path, u8); + + pub fn new() -> Result { + let std_path = Path::new("/sys/class/leds/asus::slash"); + if std_path.exists() { + info!("Found Slash LED at {:?}", std_path); + return Ok(Self { + path: std_path.to_owned(), + }); + } + + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("{}", err); + PlatformError::Udev("enumerator failed".into(), err) + })?; + + enumerator.match_subsystem("leds").map_err(|err| { + warn!("{}", err); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("{}", err); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sys = device.sysname().to_string_lossy(); + if sys.contains("slash") { + info!("Found Slash LED controls at {:?}", device.sysname()); + return Ok(Self { + path: device.syspath().to_owned(), + }); + } + } + + Err(PlatformError::MissingFunction( + "SlashLed:new(), asus::slash not found".into(), + )) + } + + pub fn is_available() -> bool { + Self::new().is_ok() + } + + pub fn path(&self) -> &Path { + &self.path + } +} diff --git a/rog-scsi/Cargo.toml b/rog-scsi/Cargo.toml index cafb9f1d2..6e8fba9ef 100644 --- a/rog-scsi/Cargo.toml +++ b/rog-scsi/Cargo.toml @@ -15,7 +15,6 @@ default = ["dbus", "ron"] dbus = ["zbus"] [dependencies] -libc.workspace = true serde.workspace = true zbus = { workspace = true, optional = true } diff --git a/rog-scsi/src/builtin_modes.rs b/rog-scsi/src/builtin_modes.rs index 0eb983cd6..4175e9655 100644 --- a/rog-scsi/src/builtin_modes.rs +++ b/rog-scsi/src/builtin_modes.rs @@ -6,8 +6,6 @@ use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Type, Value}; use crate::error::Error; -use crate::scsi::{apply_task, dir_task, mode_task, rgb_task, save_task, speed_task}; -use crate::sg::Task; #[cfg_attr(feature = "dbus", derive(Type, Value, OwnedValue))] #[derive(Debug, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)] @@ -358,39 +356,3 @@ impl Display for AuraEffect { writeln!(f, "}}") } } - -impl From<&AuraEffect> for Vec { - fn from(effect: &AuraEffect) -> Self { - let mut tasks = Vec::new(); - - tasks.append(&mut vec![ - mode_task(effect.mode as u8), - rgb_task(0, &effect.colour1.into()), - rgb_task(1, &effect.colour2.into()), - rgb_task(2, &effect.colour3.into()), - rgb_task(3, &effect.colour4.into()), - ]); - - if !matches!(effect.mode, AuraMode::Static | AuraMode::Off) { - tasks.push(speed_task(effect.speed as u8)); - } - if matches!( - effect.mode, - AuraMode::RainbowWave - | AuraMode::ChaseFade - | AuraMode::RainbowCycleChaseFade - | AuraMode::Chase - | AuraMode::RainbowCycleChase - | AuraMode::RainbowCycleWave - | AuraMode::RainbowPulseChase - ) { - tasks.push(dir_task(effect.direction as u8)); - } - - tasks.append(&mut vec![ - apply_task(), - save_task(), - ]); - tasks - } -} diff --git a/rog-scsi/src/lib.rs b/rog-scsi/src/lib.rs index 1fc382493..f300fb277 100644 --- a/rog-scsi/src/lib.rs +++ b/rog-scsi/src/lib.rs @@ -1,12 +1,9 @@ mod builtin_modes; mod error; -mod scsi; -pub mod sg; pub use builtin_modes::*; pub use error::*; use serde::{Deserialize, Serialize}; -pub use sg::{Device, Task}; pub const PROD_SCSI_ARION: &str = "1932"; @@ -43,7 +40,3 @@ impl From for &str { } } } - -pub fn open_device(path: &str) -> Result { - Device::open(path) -} diff --git a/rog-scsi/src/scsi.rs b/rog-scsi/src/scsi.rs deleted file mode 100644 index 0ae6af713..000000000 --- a/rog-scsi/src/scsi.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::sg::{Direction, Task}; - -static ENE_APPLY_VAL: u8 = 0x01; // Value for Apply Changes Register -static ENE_SAVE_VAL: u8 = 0xaa; - -static ENE_REG_MODE: u32 = 0x8021; // Mode Selection Register -static ENE_REG_SPEED: u32 = 0x8022; // Speed Control Register -static ENE_REG_DIRECTION: u32 = 0x8023; // Direction Control Register - -static ENE_REG_APPLY: u32 = 0x80a0; -static _ENE_REG_COLORS_DIRECT_V2: u32 = 0x8100; // to read the colurs -static ENE_REG_COLORS_EFFECT_V2: u32 = 0x8160; - -fn data(reg: u32, arg_count: u8) -> [u8; 16] { - let mut cdb = [0u8; 16]; - cdb[0] = 0xec; - cdb[1] = 0x41; - cdb[2] = 0x53; - cdb[3] = ((reg >> 8) & 0x00ff) as u8; - cdb[4] = (reg & 0x00ff) as u8; - cdb[5] = 0x00; - cdb[6] = 0x00; - cdb[7] = 0x00; - cdb[8] = 0x00; - cdb[9] = 0x00; - cdb[10] = 0x00; - cdb[11] = 0x00; - cdb[12] = 0x00; - cdb[13] = arg_count; // how many u8 in data packet - cdb[14] = 0x00; - cdb[15] = 0x00; - cdb -} - -pub(crate) fn rgb_task(led: u32, rgb: &[u8; 3]) -> Task { - let mut task = Task::new(); - task.set_cdb(data(led * 3 + ENE_REG_COLORS_EFFECT_V2, 3).as_slice()); - task.set_data(rgb, Direction::ToDevice); - task -} - -/// 0-13 -pub(crate) fn mode_task(mode: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_MODE, 1).as_slice()); - task.set_data(&[mode.min(13)], Direction::ToDevice); - task -} - -/// 0-4, fast to slow -pub(crate) fn speed_task(speed: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_SPEED, 1).as_slice()); - task.set_data(&[speed.min(4)], Direction::ToDevice); - task -} - -/// 0 = forward, 1 = backward -pub(crate) fn dir_task(mode: u8) -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_DIRECTION, 1).as_slice()); - task.set_data(&[mode.min(1)], Direction::ToDevice); - task -} - -pub(crate) fn apply_task() -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_APPLY, 1).as_slice()); - task.set_data(&[ENE_APPLY_VAL], Direction::ToDevice); - task -} - -pub(crate) fn save_task() -> Task { - let mut task = Task::new(); - task.set_cdb(data(ENE_REG_APPLY, 1).as_slice()); - task.set_data(&[ENE_SAVE_VAL], Direction::ToDevice); - task -} diff --git a/rog-scsi/src/sg.rs b/rog-scsi/src/sg.rs deleted file mode 100644 index c0359805e..000000000 --- a/rog-scsi/src/sg.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::ffi::c_void; -use std::fs::{File, OpenOptions}; -use std::io; -use std::os::unix::fs::OpenOptionsExt; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::path::Path; - -pub const SG_DXFER_NONE: i32 = -1; -pub const SG_DXFER_TO_DEV: i32 = -2; -pub const SG_DXFER_FROM_DEV: i32 = -3; -pub const SG_DXFER_TO_FROM_DEV: i32 = -4; - -pub const SG_INFO_OK_MASK: u32 = 0x1; -pub const SG_INFO_OK: u32 = 0x0; -pub const SG_IO: u64 = 0x2285; - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct SgIoHdr { - pub interface_id: std::os::raw::c_int, - pub dxfer_direction: std::os::raw::c_int, - pub cmd_len: u8, - pub mx_sb_len: u8, - pub iovec_count: u16, - pub dxfer_len: u32, - pub dxferp: *mut c_void, - pub cmdp: *mut u8, - pub sbp: *mut u8, - pub timeout: u32, - pub flags: u32, - pub pack_id: std::os::raw::c_int, - pub usr_ptr: *mut c_void, - pub status: u8, - pub masked_status: u8, - pub msg_status: u8, - pub sb_len_wr: u8, - pub host_status: u16, - pub driver_status: u16, - pub resid: i32, - pub duration: u32, - pub info: u32, -} - -impl Default for SgIoHdr { - fn default() -> Self { - Self { - interface_id: b'S' as std::os::raw::c_int, - dxfer_direction: SG_DXFER_NONE, - cmd_len: 0, - mx_sb_len: 0, - iovec_count: 0, - dxfer_len: 0, - dxferp: std::ptr::null_mut(), - cmdp: std::ptr::null_mut(), - sbp: std::ptr::null_mut(), - timeout: 0, - flags: 0, - pack_id: 0, - usr_ptr: std::ptr::null_mut(), - status: 0, - masked_status: 0, - msg_status: 0, - sb_len_wr: 0, - host_status: 0, - driver_status: 0, - resid: 0, - duration: 0, - info: 0, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Direction { - None, - ToDevice, - FromDevice, - ToFromDevice, -} - -impl Direction { - fn to_underlying(self) -> std::os::raw::c_int { - match self { - Direction::None => SG_DXFER_NONE, - Direction::ToDevice => SG_DXFER_TO_DEV, - Direction::FromDevice => SG_DXFER_FROM_DEV, - Direction::ToFromDevice => SG_DXFER_TO_FROM_DEV, - } - } -} - -#[derive(Clone, Debug)] -pub struct Task { - inner: SgIoHdr, - cmd: Vec, - data: Vec, - sense: Vec, -} - -impl Default for Task { - fn default() -> Self { - Self::new() - } -} - -// SAFETY: Task manages its internal buffers and SgIoHdr raw pointers. The raw pointers -// are updated prior to any ioctl execution to point directly to owned heap vectors, making -// Send and Sync safe across thread boundaries. -unsafe impl Send for Task {} -unsafe impl Sync for Task {} - -impl Task { - pub fn new() -> Self { - Task { - inner: SgIoHdr::default(), - cmd: Vec::new(), - data: Vec::new(), - sense: Vec::new(), - } - } - - /// Prepares raw internal pointers in SgIoHdr to match current buffer memory addresses. - fn sync_pointers(&mut self) { - if !self.cmd.is_empty() { - self.inner.cmdp = self.cmd.as_mut_ptr(); - self.inner.cmd_len = self.cmd.len() as u8; - } else { - self.inner.cmdp = std::ptr::null_mut(); - self.inner.cmd_len = 0; - } - - if !self.data.is_empty() { - self.inner.dxferp = self.data.as_mut_ptr() as *mut c_void; - self.inner.dxfer_len = self.data.len() as u32; - } else { - self.inner.dxferp = std::ptr::null_mut(); - self.inner.dxfer_len = 0; - } - - if !self.sense.is_empty() { - self.inner.sbp = self.sense.as_mut_ptr(); - self.inner.mx_sb_len = self.sense.len() as u8; - } else { - self.inner.sbp = std::ptr::null_mut(); - self.inner.mx_sb_len = 0; - } - } - - pub fn set_cdb(&mut self, buf: &[u8]) -> &mut Self { - self.cmd = buf.to_vec(); - self.sync_pointers(); - self - } - - pub fn set_data(&mut self, buf: &[u8], direction: Direction) -> &mut Self { - self.data = buf.to_vec(); - self.inner.dxfer_direction = direction.to_underlying(); - self.sync_pointers(); - self - } - - pub fn status(&self) -> u8 { - self.inner.status - } - - pub fn host_status(&self) -> u16 { - self.inner.host_status - } - - pub fn driver_status(&self) -> u16 { - self.inner.driver_status - } - - pub fn ok(&self) -> bool { - (self.inner.info & SG_INFO_OK_MASK) == SG_INFO_OK - } -} - -pub struct Device(File); - -impl Device { - pub fn open>(path: P) -> io::Result { - Ok(Device( - OpenOptions::new() - .read(true) - .write(true) - .custom_flags(libc::O_NONBLOCK) - .open(path)?, - )) - } - - /// Performs a synchronous SCSI IO operation via ioctl. On success the kernel - /// has written command status, sense data and any FromDevice payload back - /// into `task`, so results can be read via its accessors. - pub fn perform(&self, task: &mut Task) -> io::Result<()> { - task.sync_pointers(); - - #[cfg(target_env = "musl")] - let request = SG_IO as i32; - #[cfg(not(target_env = "musl"))] - let request: u64 = SG_IO; - - // SAFETY: The raw file descriptor is open and valid, and task has valid synced - // pointers into its own buffers, which stay alive for the duration of the - // synchronous ioctl. - let ret = unsafe { libc::ioctl(self.0.as_raw_fd(), request, &mut task.inner) }; - if ret == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } -} - -impl AsRawFd for Device { - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} diff --git a/rog-slash/Cargo.toml b/rog-slash/Cargo.toml index 1b3dfe27b..daf39b471 100644 --- a/rog-slash/Cargo.toml +++ b/rog-slash/Cargo.toml @@ -13,9 +13,8 @@ keywords = ["ROG", "ASUS", "AniMe", "Slash"] exclude = ["data"] [features] -default = ["dbus", "detect"] +default = ["dbus"] dbus = ["zbus"] -detect = ["dmi_id"] [lib] name = "rog_slash" @@ -24,5 +23,4 @@ path = "src/lib.rs" [dependencies] serde.workspace = true zbus = { workspace = true, optional = true } -dmi_id = { path = "../dmi-id", optional = true } thiserror.workspace = true diff --git a/rog-slash/src/data.rs b/rog-slash/src/data.rs index 8cd0a50e6..a2319aca3 100644 --- a/rog-slash/src/data.rs +++ b/rog-slash/src/data.rs @@ -1,7 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use dmi_id::DMIID; use serde::{Deserialize, Serialize}; #[cfg(feature = "dbus")] use zbus::zvariant::Type; @@ -56,33 +55,13 @@ impl SlashType { } } - pub fn from_dmi() -> Self { - let board_name = DMIID::new().unwrap_or_default().board_name.to_uppercase(); - if board_name.contains("G614F") { - SlashType::G614_2025 - } else if [ - "GA403W", "GA403UH", "GA403UM", "GA403UP", "GA403GM", - ] - .iter() - .any(|s| board_name.contains(s)) - { - SlashType::GA403_2025 - } else if board_name.contains("GA403") { - SlashType::GA403_2024 - } else if board_name.contains("GA605K") { - SlashType::GA605_2025 - } else if board_name.contains("GA605") { - SlashType::GA605_2024 - } else if board_name.contains("GU405") { - SlashType::GU405_2026 - } else if board_name.contains("GU606") { - SlashType::GU606_2026 - } else if board_name.contains("GU605C") { - SlashType::GU605_2025 - } else if board_name.contains("GU605") { - SlashType::GU605_2024 - } else { - SlashType::Unsupported + /// Select hidraw packet layout from the USB product id. + /// Report 0x5e vs 0x5d is a device capability, not a DMI board name. + pub fn from_usb_product(id: &str) -> Self { + match id.trim_start_matches("0x").to_ascii_uppercase().as_str() { + PROD_ID1_STR => SlashType::GA403_2024, + PROD_ID2_STR => SlashType::GA403_2025, + _ => SlashType::Unsupported, } } }