diff --git a/Cargo.lock b/Cargo.lock index 50993ad42..109be3dd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4795,7 +4795,6 @@ dependencies = [ name = "rog_scsi" version = "6.4.0" dependencies = [ - "libc", "ron", "serde", "thiserror 2.0.20", diff --git a/Cargo.toml b/Cargo.toml index 1de9691a4..8f727797e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,6 @@ glam = { version = "^0.33.5", features = ["serde"] } image = "=0.25.10" inotify = "^0.11.5" ksni = { version = "^0.3.6", default-features = false, features = ["async-io"] } -libc = "^0.2.189" log = "^0.4.33" logind-zbus = { version = "^5.3.2" } mio = "^1.2.2" diff --git a/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..84eb2b994 100644 --- a/asusd/src/aura_anime/mod.rs +++ b/asusd/src/aura_anime/mod.rs @@ -14,7 +14,6 @@ use rog_anime::usb::{ pkt_set_enable_powersave_anim, pkts_for_init, }; use rog_anime::{ActionData, AnimeDataBuffer, AnimePacketType}; -use rog_platform::hid_raw::HidRaw; use rog_platform::usb_raw::USBRaw; use tokio::sync::Mutex; @@ -23,7 +22,6 @@ use crate::error::RogError; #[derive(Debug, Clone)] pub struct AniMe { - hid: Option>>, usb: Option>>, config: Arc>, cache: AniMeConfigCached, @@ -34,13 +32,8 @@ pub struct AniMe { } impl AniMe { - pub fn new( - hid: Option>>, - usb: Option>>, - config: Arc>, - ) -> Self { + pub fn new(usb: Option>>, config: Arc>) -> Self { Self { - hid, usb, config, cache: AniMeConfigCached::default(), @@ -78,9 +71,7 @@ impl AniMe { } pub async fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { - if let Some(hid) = &self.hid { - hid.lock().await.write_bytes(message)?; - } else if let Some(usb) = &self.usb { + if let Some(usb) = &self.usb { usb.lock().await.write_bytes(message)?; } Ok(()) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index f6ebef2ca..203336ef4 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use config::AuraConfig; use config_traits::StdConfig; use log::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::hid_raw::HidRaw; +use log::{debug, warn}; +use rog_aura::keyboard::AuraLaptopUsbPackets; +use rog_aura::{AuraDeviceType, AuraEffect, LedBrightness}; +use rog_platform::DynamicLed; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; @@ -17,12 +17,19 @@ pub mod trait_impls; #[derive(Debug, Clone)] pub struct Aura { - pub hid: Option>>, + pub dynamic_global: Option>>, + pub dynamic_kbd: Option>>, + pub dynamic_lightbar: Option>>, pub backlight: Option>>, pub config: Arc>, } impl Aura { + #[must_use] + pub fn has_dynamic_lighting(&self) -> bool { + self.dynamic_kbd.is_some() || self.dynamic_global.is_some() + } + /// Initialise the device if required. pub async fn do_initialization(&self) -> Result<(), RogError> { Ok(()) @@ -36,7 +43,11 @@ impl Aura { /// this in scope then a deadlock can occur. pub async fn update_config(&self) -> Result<(), RogError> { let mut config = self.config.lock().await; - let bright = if let Some(bl) = self.backlight.as_ref() { + let bright = if let Some(dynamic) = self.dynamic_global.as_ref() { + dynamic.lock().await.get_brightness().unwrap_or_default() + } else if let Some(dynamic) = self.dynamic_kbd.as_ref() { + dynamic.lock().await.get_brightness().unwrap_or_default() + } else if let Some(bl) = self.backlight.as_ref() { bl.lock().await.get_brightness().unwrap_or_default() } else { config.brightness.into() @@ -93,41 +104,123 @@ 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 + if self.has_dynamic_lighting() + && let Some(eff_str) = mode.mode.to_dynamic_effect_str() + { + let speed = mode.speed.to_dynamic_speed(); + let dir_str = mode.direction.to_dynamic_direction_str(); + let palette = mode.to_dynamic_palette(); + + let apply_to_led = |led: &DynamicLed| -> bool { + if led.is_effect_supported(eff_str) { + let _ = led.set_speed(speed); + let _ = led.set_direction(dir_str); + let _ = led.set_palette_colors(&palette); + if let Err(e) = led.set_effect(eff_str) { + warn!("Failed to set dynamic lighting effect '{eff_str}': {e}"); + false + } else { + true + } + } else { + debug!( + "Dynamic lighting effect '{eff_str}' not supported by kernel, trying fallback" + ); + false + } + }; + + match mode.zone { + rog_aura::AuraZone::BarLeft | rog_aura::AuraZone::BarRight => { + if let Some(lb) = &self.dynamic_lightbar { + let led = lb.lock().await; + if apply_to_led(&led) { + return Ok(()); + } + } else { + debug!( + "Skipping unsupported lightbar zone in dynamic lighting unified mode" + ); + return Ok(()); + } + } + rog_aura::AuraZone::None => { + if let Some(global) = &self.dynamic_global { + let global_led = global.lock().await; + if apply_to_led(&global_led) { + return Ok(()); + } + } + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + let kbd_ok = apply_to_led(&kbd_led); + if let Some(lb) = &self.dynamic_lightbar { + let lb_led = lb.lock().await; + let _ = apply_to_led(&lb_led); + } + if kbd_ok { + return Ok(()); + } + } + } + _ => { + if let Some(kbd) = &self.dynamic_kbd { + let kbd_led = kbd.lock().await; + if apply_to_led(&kbd_led) { + return Ok(()); + } + } else if let Some(global) = &self.dynamic_global { + let global_led = global.lock().await; + if apply_to_led(&global_led) { + return Ok(()); + } + } + } } - } else if let Some(hid_raw) = &self.hid { - // Some keyboard controllers (e.g. G533QS firmware) silently drop - // short HID writes and only honour packets matching the OUTPUT - // report size declared in the HID descriptor (64 bytes for the - // 0x5d report). Pad effect/SET/APPLY here so we keep working on - // newer Strix/Zephyrus models without regressing older laptops. - const PADDED_LEN: usize = 64; - let bytes: [u8; AURA_LAPTOP_LED_MSG_LEN] = mode.into(); - let mut effect_padded = [0u8; PADDED_LEN]; - effect_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&bytes); - let mut set_padded = [0u8; PADDED_LEN]; - set_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_SET); - let mut apply_padded = [0u8; PADDED_LEN]; - apply_padded[..AURA_LAPTOP_LED_MSG_LEN].copy_from_slice(&AURA_LAPTOP_LED_APPLY); - let hid_raw = hid_raw.lock().await; - hid_raw.write_bytes(&effect_padded)?; - hid_raw.write_bytes(&set_padded)?; - // Changes won't persist unless apply is set - hid_raw.write_bytes(&apply_padded)?; - } else { - return Err(RogError::NoAuraKeyboard); } - Ok(()) + // When Dynamic Lighting is active, do not fall back to raw hidraw or TUF platform + if self.has_dynamic_lighting() { + return Err(RogError::MissingFunction( + "Dynamic lighting mode or zone not supported by kernel".to_string(), + )); + } + + // Fallback: TUF platform sysfs backlight + if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) + && let Some(platform) = &self.backlight + { + let buf = [ + 1, mode.mode as u8, mode.colour1.r, mode.colour1.g, mode.colour1.b, + mode.speed as u8, + ]; + platform.lock().await.set_kbd_rgb_mode(&buf)?; + return Ok(()); + } + + Err(RogError::NoAuraKeyboard) } pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + let mut updated = false; + if let Some(dynamic_global) = &self.dynamic_global + && dynamic_global.lock().await.set_brightness(value).is_ok() + { + updated = true; + } + if let Some(dynamic_kbd) = &self.dynamic_kbd + && dynamic_kbd.lock().await.set_brightness(value).is_ok() + { + updated = true; + } + if let Some(dynamic_lb) = &self.dynamic_lightbar { + let _ = dynamic_lb.lock().await.set_brightness(value); + } + if updated { + return Ok(()); + } + if let Some(backlight) = &self.backlight { backlight.lock().await.set_brightness(value)?; return Ok(()); @@ -140,35 +233,39 @@ impl Aura { /// Set combination state for boot animation/sleep animation/all leds/keys /// leds/side leds LED active pub async fn set_power_states(&self, config: &AuraConfig) -> Result<(), RogError> { - if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { - if let Some(backlight) = &self.backlight { - // TODO: tuf bool array - let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; - } - } else if let Some(hid_raw) = &self.hid { - let hid_raw = hid_raw.lock().await; - if let Some(p) = config.enabled.states.first() - && p.zone == PowerZones::Ally - { - let msg = [ - 0x5d, - 0xd1, - 0x09, - 0x01, - p.new_to_byte() as u8, - 0x0, - 0x0, - ]; - hid_raw.write_bytes(&msg)?; - return Ok(()); + if let Some(dynamic_led) = self.dynamic_global.as_ref().or(self.dynamic_kbd.as_ref()) { + let kbd = dynamic_led.lock().await; + if kbd.has_power_states() { + let mut states = Vec::new(); + for state in &config.enabled.states { + if state.boot { + states.push("boot"); + } + if state.awake { + states.push("awake"); + } + if state.sleep { + states.push("sleep"); + } + if state.shutdown { + states.push("shutdown"); + } + } + let states_str = states.join(" "); + if let Err(e) = kbd.set_power_states(&states_str) { + warn!("Failed to set power states via dynamic lighting: {e}"); + } else { + return Ok(()); + } } + } - let bytes = config.enabled.to_bytes(config.led_type); - let msg = [ - 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], - ]; - hid_raw.write_bytes(&msg)?; + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + && let Some(backlight) = &self.backlight + { + // TODO: tuf bool array + let buf = config.enabled.to_bytes(config.led_type); + backlight.lock().await.set_kbd_rgb_state(&buf)?; } Ok(()) } @@ -186,27 +283,7 @@ impl Aura { config.write(); } - let pkt_type = effect[0][1]; - const PER_KEY_TYPE: u8 = 0xbc; - - if let Some(hid_raw) = &self.hid { - let hid_raw = hid_raw.lock().await; - if pkt_type != PER_KEY_TYPE { - config.per_key_mode_active = false; - hid_raw.write_bytes(&effect[0])?; - hid_raw.write_bytes(&AURA_LAPTOP_LED_SET)?; - // hid_raw.write_bytes(&LED_APPLY)?; - } else { - if !config.per_key_mode_active { - let init = LedUsbPackets::get_init_msg(); - hid_raw.write_bytes(&init)?; - config.per_key_mode_active = true; - } - for row in effect.iter() { - hid_raw.write_bytes(row)?; - } - } - } else if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) + if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) && let Some(tuf) = &self.backlight { for row in effect.iter() { @@ -217,25 +294,48 @@ impl Aura { 0, 0, r, g, b, 0, ])?; } + return Ok(()); } - Ok(()) - } - pub async fn fix_ally_power(&mut self) -> Result<(), RogError> { - if self.config.lock().await.led_type == AuraDeviceType::Ally - && let Some(hid_raw) = &self.hid - { - let mut config = self.config.lock().await; - if config.ally_fix.is_none() { - let msg = [ - 0x5d, 0xbd, 0x01, 0xff, 0xff, 0xff, 0xff, - ]; - hid_raw.lock().await.write_bytes(&msg)?; - info!("Reset Ally power settings to base"); - config.ally_fix = Some(true); + let dynamic_led = self.dynamic_kbd.as_ref().or(self.dynamic_global.as_ref()); + if let Some(dynamic) = dynamic_led { + let dynamic = dynamic.lock().await; + let led_count = dynamic.get_led_count().unwrap_or(168) as usize; + let expected_len = led_count * 3; + let mut rgb_buf = Vec::with_capacity(expected_len); + + if effect.len() == 1 && led_count == 4 { + // Zoned keyboard (4 zones: left, left-mid, right-mid, right) + if let Some(row) = effect.first() + && let Some(payload) = row.get(9..21) + { + rgb_buf.extend_from_slice(payload); + } + } else { + // Per-key keyboard + for row in effect.iter() { + if row.len() >= 9 { + let num_leds = row.get(7).copied().unwrap_or(16) as usize; + let payload_len = num_leds * 3; + if let Some(payload) = row.get(9..9 + payload_len) { + rgb_buf.extend_from_slice(payload); + } + } + } } - config.write(); + + if !rgb_buf.is_empty() { + rgb_buf.resize(expected_len, 0); + dynamic.write_direct(&rgb_buf)?; + config.per_key_mode_active = true; + } + return Ok(()); } + + Err(RogError::NoAuraKeyboard) + } + + pub async fn fix_ally_power(&mut self) -> Result<(), RogError> { Ok(()) } } diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 90629044d..79270e4c8 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -97,13 +97,62 @@ impl AuraZbus { #[zbus(property)] async fn supported_basic_modes(&self) -> Result, ZbErr> { let config = self.0.config.lock().await; + if self.0.has_dynamic_lighting() { + let led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + if let Some(led) = led_lock { + let mut modes = Vec::new(); + for mode in config.builtins.keys() { + if let Some(eff_str) = mode.to_dynamic_effect_str() + && led.is_effect_supported(eff_str) + { + modes.push(*mode); + } + } + return Ok(modes); + } + } Ok(config.builtins.keys().cloned().collect()) } #[zbus(property)] async fn supported_basic_zones(&self) -> Result, ZbErr> { let config = self.0.config.lock().await; - Ok(config.support_data.basic_zones.clone()) + if self.0.has_dynamic_lighting() { + // If the kernel exposes only aura:global (unified chassis), there are + // no independent basic zones; the entire lighting is driven as a single unit. + if self.0.dynamic_kbd.is_none() && self.0.dynamic_lightbar.is_none() { + return Ok(vec![]); + } + let mut zones = config.support_data.basic_zones.clone(); + if self.0.dynamic_lightbar.is_none() { + zones.retain(|z| !matches!(z, AuraZone::BarLeft | AuraZone::BarRight)); + } else { + if !zones.contains(&AuraZone::BarLeft) { + zones.push(AuraZone::BarLeft); + } + if !zones.contains(&AuraZone::BarRight) { + zones.push(AuraZone::BarRight); + } + } + Ok(zones) + } else { + let mut zones = config.support_data.basic_zones.clone(); + if self.0.dynamic_lightbar.is_some() { + if !zones.contains(&AuraZone::BarLeft) { + zones.push(AuraZone::BarLeft); + } + if !zones.contains(&AuraZone::BarRight) { + zones.push(AuraZone::BarRight); + } + } + Ok(zones) + } } #[zbus(property)] @@ -166,10 +215,39 @@ impl AuraZbus { #[zbus(property)] async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> { let mut config = self.0.config.lock().await; - if !config.support_data.basic_modes.contains(&effect.mode) - || effect.zone != AuraZone::None - && !config.support_data.basic_zones.contains(&effect.zone) - { + let (is_mode_supported, is_zone_supported) = if self.0.has_dynamic_lighting() { + let mode_ok = if let Some(eff_str) = effect.mode.to_dynamic_effect_str() { + let led_lock = if let Some(global) = &self.0.dynamic_global { + Some(global.lock().await) + } else if let Some(kbd) = &self.0.dynamic_kbd { + Some(kbd.lock().await) + } else { + None + }; + led_lock.is_some_and(|l| l.is_effect_supported(eff_str)) + } else { + false + }; + let zone_ok = match effect.zone { + AuraZone::None => true, + AuraZone::BarLeft | AuraZone::BarRight => self.0.dynamic_lightbar.is_some(), + _ => { + self.0.dynamic_kbd.is_some() + && config.support_data.basic_zones.contains(&effect.zone) + } + }; + (mode_ok, zone_ok) + } else { + ( + config.support_data.basic_modes.contains(&effect.mode), + effect.zone == AuraZone::None + || config.support_data.basic_zones.contains(&effect.zone) + || (self.0.dynamic_lightbar.is_some() + && matches!(effect.zone, AuraZone::BarLeft | AuraZone::BarRight)), + ) + }; + + if !is_mode_supported || !is_zone_supported { return Err(ZbErr::NotSupported(format!( "The Aura effect is not supported: {effect:?}" ))); diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index d809bd744..115d50a95 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -4,14 +4,15 @@ // - 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 rog_slash::SlashType; use tokio::sync::Mutex; use udev::{Device, MonitorBuilder}; use zbus::Connection; @@ -97,7 +98,6 @@ pub struct AsusDevice { pub struct DeviceManager { _dbus_connection: Connection, - _hid_handles: Arc>>>>, } /// Returns true if this hidraw device is a non-Aura interface on the @@ -132,30 +132,9 @@ fn is_non_aura_1ce6_interface(device: &Device) -> bool { } impl DeviceManager { - #[allow(clippy::type_complexity)] - async fn get_or_create_hid_handle( - handles: &Arc>>>>, - endpoint: &Device, - ) -> Result<(Arc>, String), RogError> { - let dev_node = endpoint - .devnode() - .ok_or_else(|| RogError::MissingFunction("hidraw devnode missing".to_string()))?; - let key = dev_node.to_string_lossy().to_string(); - - if let Some(existing) = handles.lock().await.get(&key).cloned() { - return Ok((existing, key)); - } - - let hidraw = HidRaw::from_device(endpoint.clone())?; - let handle = Arc::new(Mutex::new(hidraw)); - handles.lock().await.insert(key.clone(), handle.clone()); - Ok((handle, key)) - } - async fn init_hid_devices( connection: &Connection, device: Device, - handles: Arc>>>>, ) -> Result, RogError> { let mut devices = Vec::new(); if let Some(usb_device) = device.parent_with_subsystem_devtype("usb", "usb_device")? @@ -170,90 +149,76 @@ impl DeviceManager { // So let's see what we have and: // 1. Generate an interface path // 2. Create the device - // Use the top-level endpoint, not the parent - if let Ok((dev, hid_key)) = Self::get_or_create_hid_handle(&handles, &device).await { - debug!("Testing device {usb_id:?}"); - // SLASH DEVICE - if let Ok(dev_type) = - DeviceHandle::new_slash_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Slash(slash) = dev_type.clone() - { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_slash()); - let ctrl = SlashZbus::new(slash); - if ctrl - .start_tasks(connection, path.clone()) - .await - .map_err(|e| { - error!("Failed to start Slash tasks: {e:?}, not adding this device") - }) - .is_ok() - { - devices.push(AsusDevice { - device: dev_type, - dbus_path: path, - hid_key: Some(hid_key.clone()), - }); - } - } - // ANIME MATRIX DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_anime_hid(dev.clone(), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::AniMe(anime) = dev_type.clone() + let usb_id_str = usb_id.to_str().unwrap_or_default(); + let aura_type = AuraDeviceType::from(usb_id_str); + if matches!( + aura_type, + AuraDeviceType::LaptopKeyboard2021 + | AuraDeviceType::LaptopKeyboardPre2021 + | AuraDeviceType::LaptopKeyboardTuf + | AuraDeviceType::Ally + ) { + // AURA LAPTOP DEVICE - driven by Dynamic Lighting / sysfs, no hidraw needed + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(usb_id_str).await + && let DeviceHandle::Aura(aura) = dev_type.clone() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_anime()); - let ctrl = AniMeZbus::new(anime); + let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); + let ctrl = AuraZbus::new(aura); if ctrl .start_tasks(connection, path.clone()) .await .map_err(|e| { - error!("Failed to start AniMe tasks: {e:?}, not adding this device") + error!("Failed to start Aura tasks: {e:?}, not adding this device") }) .is_ok() { devices.push(AsusDevice { device: dev_type, dbus_path: path, - hid_key: Some(hid_key.clone()), + hid_key: None, }); } } - // AURA LAPTOP DEVICE - if let Ok(dev_type) = - DeviceHandle::maybe_laptop_aura(Some(dev), usb_id.to_str().unwrap_or_default()) - .await - && let DeviceHandle::Aura(aura) = dev_type.clone() + return Ok(devices); + } + + // Check for Slash device via sysfs SlashLed / USB + let slash_type = SlashType::from_dmi(); + if !matches!(slash_type, SlashType::Unsupported) + && slash_type + .prod_id_str() + .to_lowercase() + .trim_start_matches("0x") + == usb_id_str + { + if let Ok(dev_type) = DeviceHandle::maybe_slash().await + && let DeviceHandle::Slash(slash) = dev_type.clone() { - let path = dbus_path_for_dev(&usb_device).unwrap_or(dbus_path_for_tuf()); - let ctrl = AuraZbus::new(aura); + 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 Aura tasks: {e:?}, not adding this device") + 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), + hid_key: None, }); } } - } else { - warn!("Failed to initialise shared hid handle for {usb_id:?}"); + return Ok(devices); } } Ok(devices) } /// To be called on daemon startup - async fn init_all_hid( - connection: &Connection, - handles: Arc>>>>, - ) -> Result, RogError> { + async fn init_all_hid(connection: &Connection) -> Result, RogError> { // Ensure we only process one hidraw interface per physical USB device. // A USB device can expose multiple HID interfaces (and thus multiple hidraw nodes). // Processing more than one causes duplicate device initialisation which can @@ -290,37 +255,12 @@ impl DeviceManager { } } - devices.append(&mut Self::init_hid_devices(connection, device, handles.clone()).await?); + devices.append(&mut Self::init_hid_devices(connection, device).await?); } Ok(devices) } - /// Resolve the `/dev/sgN` (scsi_generic) node backing a block device. - /// - /// Walks up from the block device to its owning scsi_device and reads the - /// `scsi_generic/sgN` child. Works for whole-disk (`/dev/sda`) and - /// partition (`/dev/sda1`) nodes alike, since the scsi_device is a common - /// ancestor. Returns None if no sg node exists (e.g. the `sg` module is - /// not loaded). - fn sg_node_for_block(device: &Device) -> Option { - let mut current = device.parent(); - while let Some(d) = current { - if let Ok(entries) = std::fs::read_dir(d.syspath().join("scsi_generic")) { - for entry in entries.flatten() { - if let Some(name) = entry.file_name().to_str() { - let node = format!("/dev/{name}"); - if std::path::Path::new(&node).exists() { - return Some(node); - } - } - } - } - current = d.parent(); - } - None - } - async fn init_scsi( connection: &Connection, device: &Device, @@ -335,40 +275,9 @@ impl DeviceManager { .property_value("ID_MODEL_ID") .unwrap_or_default() .to_string_lossy(); - // SG_IO with vendor commands on the block node (/dev/sdX) - // requires CAP_SYS_RAWIO, which the hardened asusd unit drops - // (every ioctl EPERMs and is silently swallowed by write_effect). - // The scsi_generic /dev/sgN node gates access at open() via - // file permissions instead, so it works with no capabilities, - // the same path sg3_utils / OpenRGB use. - // - // On hotplug the sg node can appear just after the block node, - // so retry briefly before falling back to the block device - // (which would EPERM). At startup the node already exists, so - // the first attempt succeeds with no delay. - let mut sg_node = None; - for attempt in 0..8u8 { - if let Some(sg) = Self::sg_node_for_block(device) { - sg_node = Some(sg); - break; - } - if attempt < 7 { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } - let dev_str = match sg_node { - Some(sg) => Some(sg), - None => { - warn!( - "No /dev/sgN for SCSI device after retries; falling back to block \ - node {:?} (SG_IO will EPERM unless asusd has CAP_SYS_RAWIO)", - dev_node - ); - dev_node.as_os_str().to_str().map(|s| s.to_string()) - } - }; - if let Some(dev_str) = dev_str - && let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await + + let dev_str = dev_node.to_string_lossy(); + if let Ok(dev_type) = DeviceHandle::maybe_scsi(&dev_str, &prod_id).await && let DeviceHandle::Scsi(scsi) = dev_type.clone() { let ctrl = ScsiZbus::new(scsi); @@ -388,6 +297,7 @@ impl DeviceManager { } } } + None } @@ -430,13 +340,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 +357,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); @@ -515,7 +422,7 @@ impl DeviceManager { ); if product_name.contains("TUF") || product_family.contains("TUF") { info!("TUF laptop, try using sysfs backlight control"); - if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(None, "tuf").await + if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura("tuf").await && let DeviceHandle::Aura(aura) = dev_type.clone() { let path = dbus_path_for_tuf(); @@ -547,19 +454,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 +492,6 @@ impl DeviceManager { let devices = devices.clone(); let conn_copy = conn_copy.clone(); - let hid_handles = hid_handles_thread.clone(); rt.block_on(async move { // SCSCI devs if subsys == "block" { @@ -706,11 +609,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 +631,9 @@ impl DeviceManager { if is_non_aura_1ce6_interface(&evdev) { return Ok(()); } - if let Ok(mut new_devs) = - Self::init_hid_devices(&conn_copy, evdev, hid_handles.clone()) - .await - .map_err(|e| error!("Couldn't add new device: {e:?}")) + if let Ok(mut new_devs) = Self::init_hid_devices(&conn_copy, evdev) + .await + .map_err(|e| error!("Couldn't add new device: {e:?}")) { devices.lock().await.append(&mut new_devs); } diff --git a/asusd/src/aura_scsi/mod.rs b/asusd/src/aura_scsi/mod.rs index 5e77ae47c..b62e00742 100644 --- a/asusd/src/aura_scsi/mod.rs +++ b/asusd/src/aura_scsi/mod.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use config::ScsiConfig; -use rog_scsi::{AuraEffect, Device, Task}; +use rog_platform::ScsiLed; +use rog_scsi::{AuraEffect, AuraMode, Direction}; use tokio::sync::{Mutex, MutexGuard}; use crate::error::RogError; @@ -11,13 +12,17 @@ pub mod trait_impls; #[derive(Clone)] pub struct ScsiAura { - device: Arc>, - config: Arc>, + pub led: ScsiLed, + pub config: Arc>, } impl ScsiAura { - pub fn new(device: Arc>, config: Arc>) -> Self { - Self { device, config } + pub fn new(led: ScsiLed, config: Arc>) -> Self { + Self { led, config } + } + + pub fn led(&self) -> &ScsiLed { + &self.led } pub async fn lock_config(&self) -> MutexGuard<'_, ScsiConfig> { @@ -25,13 +30,75 @@ impl ScsiAura { } pub async fn write_effect(&self, effect: &AuraEffect) -> Result<(), RogError> { - let mut tasks: Vec = effect.into(); - for task in &mut tasks { - // Surface the ioctl errno instead of dropping it — an EPERM/EIO - // here was previously invisible, so asusd reported success while - // no SCSI traffic ever reached the device. - if let Err(e) = self.device.lock().await.perform(task) { - log::warn!("SCSI perform failed: {e}"); + Self::write_kernel_effect(&self.led, effect) + } + + fn write_kernel_effect(led: &ScsiLed, effect: &AuraEffect) -> Result<(), RogError> { + match effect.mode { + AuraMode::Off => { + led.set_effect("off").map_err(RogError::Platform)?; + } + AuraMode::Static => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_effect("static").map_err(RogError::Platform)?; + } + AuraMode::Breathe => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("breathing").map_err(RogError::Platform)?; + } + AuraMode::Flashing => { + let colors = [ + (effect.colour1.r, effect.colour1.g, effect.colour1.b), + (effect.colour2.r, effect.colour2.g, effect.colour2.b), + (effect.colour3.r, effect.colour3.g, effect.colour3.b), + (effect.colour4.r, effect.colour4.g, effect.colour4.b), + ]; + led.set_palette_colors(&colors) + .map_err(RogError::Platform)?; + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("strobe").map_err(RogError::Platform)?; + } + AuraMode::RainbowCycle + | AuraMode::RainbowCycleBreathe + | AuraMode::RainbowPulseChase + | AuraMode::RandomFlicker + | AuraMode::DoubleFade => { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + led.set_effect("spectrum_cycle") + .map_err(RogError::Platform)?; + } + AuraMode::RainbowWave + | AuraMode::Chase + | AuraMode::ChaseFade + | AuraMode::RainbowCycleChase + | AuraMode::RainbowCycleChaseFade + | AuraMode::RainbowCycleWave => { + led.set_speed(effect.speed as u32) + .map_err(RogError::Platform)?; + let dir = match effect.direction { + Direction::Forward => "right", + Direction::Reverse => "left", + }; + led.set_direction(dir).map_err(RogError::Platform)?; + led.set_effect("rainbow").map_err(RogError::Platform)?; } } Ok(()) diff --git a/asusd/src/aura_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..f37e62585 100644 --- a/asusd/src/aura_slash/trait_impls.rs +++ b/asusd/src/aura_slash/trait_impls.rs @@ -1,10 +1,5 @@ use config_traits::StdConfig; use log::{debug, error, warn}; -use rog_slash::usb::{ - slash_pkt_battery_saver, slash_pkt_boot, slash_pkt_enable, slash_pkt_lid_closed, - slash_pkt_low_battery, slash_pkt_options, slash_pkt_save, slash_pkt_set_mode, - slash_pkt_shutdown, slash_pkt_sleep, -}; use rog_slash::{DeviceState, SlashMode}; use zbus::zvariant::OwnedObjectPath; use zbus::{Connection, interface}; @@ -26,7 +21,6 @@ impl SlashZbus { connection: &Connection, path: OwnedObjectPath, ) -> Result<(), RogError> { - // let task = zbus.clone(); self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); @@ -58,25 +52,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 +75,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 +95,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 +110,7 @@ impl SlashZbus { Ok(config.display_mode as u8) } - /// Set interval between slash animations (0-255) + /// Set animation mode #[zbus(property)] async fn set_mode(&self, mode: u8) -> zbus::Result<()> { let mode = SlashMode::try_from(mode).map_err(|err| { @@ -151,12 +118,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 +131,6 @@ impl SlashZbus { } /// Get the device state as stored by asusd - // #[zbus(property)] async fn device_state(&self) -> DeviceState { let config = self.0.lock_config().await; DeviceState::from(&*config) @@ -179,9 +145,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_boot(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_boot(config.slash_type, enable)) - .await?; config.show_on_boot = enable; config.write(); Ok(()) @@ -196,9 +159,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_sleep(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_sleep(config.slash_type, enable)) - .await?; config.show_on_sleep = enable; config.write(); Ok(()) @@ -213,9 +173,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_shutdown(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_shutdown(config.slash_type, enable)) - .await?; config.show_on_shutdown = enable; config.write(); Ok(()) @@ -230,9 +187,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_battery(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_battery_saver(config.slash_type, enable)) - .await?; config.show_on_battery = enable; config.write(); Ok(()) @@ -247,9 +201,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_battery_warning(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_low_battery(config.slash_type, enable)) - .await?; config.show_battery_warning = enable; config.write(); Ok(()) @@ -264,12 +215,6 @@ impl SlashZbus { #[zbus(property)] async fn set_show_on_lid_closed(&self, enable: bool) -> zbus::Result<()> { let mut config = self.0.lock_config().await; - self.0 - .write_bytes(&slash_pkt_lid_closed(config.slash_type, enable)) - .await?; - self.0 - .write_bytes(&slash_pkt_save(config.slash_type)) - .await?; config.show_on_lid_closed = enable; config.write(); Ok(()) @@ -280,40 +225,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..cc1bd48e9 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -1,15 +1,17 @@ use std::sync::Arc; use config_traits::{StdConfig, StdConfigLoad}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use rog_anime::AnimeType; use rog_anime::error::AnimeError; use rog_anime::usb::get_anime_type; use rog_aura::AuraDeviceType; -use rog_platform::hid_raw::HidRaw; +use rog_platform::DynamicLed; +use rog_platform::ScsiLed; +use rog_platform::SlashLed; use rog_platform::keyboard_led::KeyboardBacklight; use rog_platform::usb_raw::USBRaw; -use rog_scsi::{ScsiType, open_device}; +use rog_scsi::ScsiType; use rog_slash::SlashType; use rog_slash::error::SlashError; use tokio::sync::Mutex; @@ -27,7 +29,6 @@ use crate::error::RogError; pub enum _DeviceHandle { /// The AniMe devices require USBRaw as they are not HID devices Usb(USBRaw), - HidRaw(HidRaw), LedClass(KeyboardBacklight), /// TODO MulticolourLed, @@ -41,93 +42,33 @@ 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"); + /// Try Slash sysfs LED. If one exists it is initialised and returned. + pub async fn maybe_slash() -> Result { + debug!("Testing for Slash"); let slash_type = SlashType::from_dmi(); - if matches!(slash_type, SlashType::Unsupported) - || slash_type - .prod_id_str() - .to_lowercase() - .trim_start_matches("0x") - != prod_id - { - log::info!("Unknown or invalid slash: {prod_id:?}, skipping"); - return Err(RogError::NotFound("No slash device".to_string())); + if matches!(slash_type, SlashType::Unsupported) { + return Err(RogError::Slash(SlashError::NoDevice)); } - info!("Found slash type {slash_type:?}: {prod_id}"); + 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))); + 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 +83,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,26 +96,32 @@ impl DeviceHandle { } pub async fn maybe_scsi(dev_node: &str, prod_id: &str) -> Result { - debug!("Testing for SCSI"); - let prod_id = ScsiType::from(prod_id); - if prod_id == ScsiType::Unsupported { - log::info!("Unknown or invalid SCSI: {prod_id:?}, skipping"); + let scsi_type = ScsiType::from(prod_id); + if scsi_type == ScsiType::Unsupported { + log::info!("Unknown or invalid SCSI: {scsi_type:?}, skipping"); return Err(RogError::NotFound("No SCSI device".to_string())); } - info!("Found SCSI device {prod_id:?} on {dev_node}"); + + let led = ScsiLed::find_for_dev(dev_node) + .or_else(|_| ScsiLed::new()) + .map_err(|e| { + log::warn!("No SCSI Dynamic Lighting device found for {dev_node}: {e}"); + RogError::NotFound("No SCSI Dynamic Lighting device found".to_string()) + })?; + + info!( + "Found SCSI Dynamic Lighting device {scsi_type:?} on {:?}", + led.path() + ); let mut config = ScsiConfig::new().load(); config.dev_type = AuraDeviceType::ScsiExtDisk; - let dev = Arc::new(Mutex::new(open_device(dev_node)?)); - let scsi = ScsiAura::new(dev, Arc::new(Mutex::new(config))); + let scsi = ScsiAura::new(led, Arc::new(Mutex::new(config))); scsi.do_initialization().await?; Ok(Self::Scsi(scsi)) } - pub async fn maybe_laptop_aura( - device: Option>>, - prod_id: &str, - ) -> Result { + pub async fn maybe_laptop_aura(prod_id: &str) -> Result { debug!("Testing for laptop aura"); let aura_type = AuraDeviceType::from(prod_id); if !matches!( @@ -197,11 +143,48 @@ 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) + }; + + if dynamic_global.is_none() + && dynamic_kbd.is_none() + && dynamic_lightbar.is_none() + && backlight.is_none() + { + debug!("Neither Dynamic Lighting nor sysfs backlight detected"); + return Err(RogError::NotFound( + "No dynamic lighting or sysfs backlight found".to_string(), + )); + } + // Load saved mode, colours, brightness, power from disk; apply on reload let mut config = AuraConfig::load_and_update_config(prod_id); config.led_type = aura_type; let aura = Aura { - hid: device, + dynamic_global, + dynamic_kbd, + dynamic_lightbar, backlight, config: Arc::new(Mutex::new(config)), }; diff --git a/rog-aura/src/builtin_modes.rs b/rog-aura/src/builtin_modes.rs index d34f2b0f3..e6ab38c54 100644 --- a/rog-aura/src/builtin_modes.rs +++ b/rog-aura/src/builtin_modes.rs @@ -199,6 +199,24 @@ impl From for u8 { } } } + +impl Speed { + pub const fn to_dynamic_speed(&self) -> u32 { + match self { + Self::Low => 0, + Self::Med => 1, + Self::High => 2, + } + } + + pub const fn from_dynamic_speed(val: u32) -> Self { + match val { + 0 => Self::Low, + 2 => Self::High, + _ => Self::Med, + } + } +} /// Used for Rainbow mode. /// /// Enum corresponds to the required integer value @@ -248,6 +266,26 @@ impl From for i32 { } } +impl Direction { + pub const fn to_dynamic_direction_str(&self) -> &'static str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Up => "up", + Self::Down => "down", + } + } + + pub fn from_dynamic_direction_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "left" => Self::Left, + "up" => Self::Up, + "down" => Self::Down, + _ => Self::Right, + } + } +} + /// Enum of modes that convert to the actual number required by a USB HID packet #[cfg_attr( feature = "dbus", @@ -359,6 +397,32 @@ impl From for AuraModeNum { } } +impl AuraModeNum { + /// Return the corresponding Dynamic Lighting effect name, if available. + pub const fn to_dynamic_effect_str(&self) -> Option<&'static str> { + match self { + Self::Static => Some("static"), + Self::Breathe => Some("breathing"), + Self::RainbowCycle => Some("spectrum_cycle"), + Self::RainbowWave => Some("rainbow"), + Self::Pulse | Self::Flash => Some("strobe"), + _ => None, + } + } + + /// Parse a Dynamic Lighting effect name into an `AuraModeNum`. + pub fn from_dynamic_effect_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "static" => Some(Self::Static), + "breathing" => Some(Self::Breathe), + "spectrum_cycle" => Some(Self::RainbowCycle), + "rainbow" => Some(Self::RainbowWave), + "strobe" => Some(Self::Pulse), + _ => None, + } + } +} + #[cfg(feature = "dbus")] impl zbus::zvariant::Basic for AuraModeNum { const SIGNATURE_CHAR: char = 'u'; @@ -404,7 +468,7 @@ impl FromStr for AuraZone { "3" | "three" => Ok(AuraZone::Key3), "4" | "four" => Ok(AuraZone::Key4), "5" | "logo" => Ok(AuraZone::Logo), - "6" | "lightbar-left" => Ok(AuraZone::BarLeft), + "6" | "lightbar-left" | "lightbar" | "bar" => Ok(AuraZone::BarLeft), "7" | "lightbar-right" => Ok(AuraZone::BarRight), _ => Err(Error::ParseSpeed), } @@ -469,6 +533,16 @@ impl AuraEffect { pub fn zone(&self) -> AuraZone { self.zone } + + /// Convert the effect colours to an array of RGB tuples for Dynamic Lighting palette. + pub fn to_dynamic_palette(&self) -> Vec<(u8, u8, u8)> { + let mut p = Vec::with_capacity(2); + p.push((self.colour1.r, self.colour1.g, self.colour1.b)); + if self.colour2.r != 0 || self.colour2.g != 0 || self.colour2.b != 0 { + p.push((self.colour2.r, self.colour2.g, self.colour2.b)); + } + p + } } impl Default for AuraEffect { @@ -689,4 +763,88 @@ mod tests { capture[..9] ); } + + #[test] + fn test_dynamic_lighting_conversions() { + assert_eq!(AuraModeNum::Static.to_dynamic_effect_str(), Some("static")); + assert_eq!( + AuraModeNum::Breathe.to_dynamic_effect_str(), + Some("breathing") + ); + assert_eq!( + AuraModeNum::RainbowCycle.to_dynamic_effect_str(), + Some("spectrum_cycle") + ); + assert_eq!( + AuraModeNum::RainbowWave.to_dynamic_effect_str(), + Some("rainbow") + ); + assert_eq!(AuraModeNum::Pulse.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Flash.to_dynamic_effect_str(), Some("strobe")); + assert_eq!(AuraModeNum::Star.to_dynamic_effect_str(), None); + + assert_eq!( + AuraModeNum::from_dynamic_effect_str("static"), + Some(AuraModeNum::Static) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("breathing"), + Some(AuraModeNum::Breathe) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("spectrum_cycle"), + Some(AuraModeNum::RainbowCycle) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("rainbow"), + Some(AuraModeNum::RainbowWave) + ); + assert_eq!( + AuraModeNum::from_dynamic_effect_str("strobe"), + Some(AuraModeNum::Pulse) + ); + assert_eq!(AuraModeNum::from_dynamic_effect_str("unknown"), None); + + assert_eq!(Speed::Low.to_dynamic_speed(), 0); + assert_eq!(Speed::Med.to_dynamic_speed(), 1); + assert_eq!(Speed::High.to_dynamic_speed(), 2); + assert_eq!(Speed::from_dynamic_speed(0), Speed::Low); + assert_eq!(Speed::from_dynamic_speed(1), Speed::Med); + assert_eq!(Speed::from_dynamic_speed(2), Speed::High); + + assert_eq!(Direction::Right.to_dynamic_direction_str(), "right"); + assert_eq!(Direction::Left.to_dynamic_direction_str(), "left"); + assert_eq!(Direction::Up.to_dynamic_direction_str(), "up"); + assert_eq!(Direction::Down.to_dynamic_direction_str(), "down"); + assert_eq!( + Direction::from_dynamic_direction_str("left"), + Direction::Left + ); + assert_eq!( + Direction::from_dynamic_direction_str("right"), + Direction::Right + ); + + let effect = AuraEffect { + colour1: Colour { + r: 0xff, + g: 0x10, + b: 0x20, + }, + colour2: Colour { + r: 0x00, + g: 0x30, + b: 0x40, + }, + ..Default::default() + }; + let palette = effect.to_dynamic_palette(); + assert_eq!( + palette, + vec![ + (0xff, 0x10, 0x20), + (0x00, 0x30, 0x40) + ] + ); + } } diff --git a/rog-platform/src/dynamic_led.rs b/rog-platform/src/dynamic_led.rs new file mode 100644 index 000000000..e3b48c312 --- /dev/null +++ b/rog-platform/src/dynamic_led.rs @@ -0,0 +1,148 @@ +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 name.starts_with("aura:") || syspath.join("effect_index").exists() { + info!("Found Dynamic Lighting LED device at {:?}", sysname); + return Ok(Self { + path: syspath.to_path_buf(), + }); + } + } + } + + 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 + } + + /// Read and parse space-separated list of supported effects from `effect_index`. + pub fn get_supported_effects_list(&self) -> Result> { + let raw = self.get_effect_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Check if a given effect mode string is supported by the kernel driver. + pub fn is_effect_supported(&self, effect: &str) -> bool { + self.get_supported_effects_list() + .map(|list| list.iter().any(|e| e == effect)) + .unwrap_or(false) + } + + /// Read and parse space-separated list of supported directions from `direction_index`. + pub fn get_supported_directions_list(&self) -> Result> { + let raw = self.get_direction_index()?; + Ok(raw.split_whitespace().map(String::from).collect()) + } + + /// Write raw RGB byte buffer directly to the `direct_buffer` (or `direct`) binary attribute. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + let direct_path = if self.path.join("direct_buffer").exists() { + self.path.join("direct_buffer") + } else { + self.path.join("direct") + }; + std::fs::write(&direct_path, data) + .map_err(|e| PlatformError::IoPath(direct_path.to_string_lossy().into_owned(), e)) + } + + /// Write palette colors as formatted `"#RRGGBB #RRGGBB ..."` string to `effects_palette`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + self.set_effects_palette(&palette_str) + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn test_palette_colors_formatting() { + let colors = [ + (255, 0, 128), + (0, 255, 64), + ]; + let formatted: Vec = colors + .iter() + .map(|(r, g, b)| format!("#{r:02x}{g:02x}{b:02x}")) + .collect(); + let palette_str = formatted.join(" "); + assert_eq!(palette_str, "#ff0080 #00ff40"); + } +} diff --git a/rog-platform/src/lib.rs b/rog-platform/src/lib.rs index dec0bb861..43efaeffb 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"); diff --git a/rog-platform/src/scsi_led.rs b/rog-platform/src/scsi_led.rs new file mode 100644 index 000000000..b25503629 --- /dev/null +++ b/rog-platform/src/scsi_led.rs @@ -0,0 +1,178 @@ +use std::path::Path; + +use log::{info, warn}; + +use crate::dynamic_led::DynamicLed; +use crate::error::{PlatformError, Result}; + +/// Generic control interface for ASUS Aura SCSI-attached lighting devices +/// backed by the kernel `leds-asus-aura-scsi` Dynamic Lighting driver. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] +pub struct ScsiLed { + dynamic: DynamicLed, +} + +impl ScsiLed { + /// Discover the first available ASUS SCSI dynamic lighting LED node. + pub fn new() -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("ScsiLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("ScsiLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("ScsiLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = device.sysname().to_string_lossy(); + if sysname.contains("asus-scsi") + || sysname.contains("asus-aura-scsi") + || sysname.contains("asus-arion") + { + info!( + "Found ASUS SCSI Dynamic Lighting LED device at {:?}", + sysname + ); + let dynamic = DynamicLed::find(&sysname)?; + return Ok(Self { dynamic }); + } + } + + Err(PlatformError::MissingFunction( + "ScsiLed::new(): no asus scsi dynamic LED device found".into(), + )) + } + + /// Find a `ScsiLed` instance corresponding to a given SCSI block device. + pub fn find_for_block(device: &udev::Device) -> Result { + // Walk up from block device to its scsi parent to get H:C:T:L (e.g. "2:0:0:0") + let mut current = device.parent(); + let mut hctl = None; + while let Some(d) = current { + if let Some(sub) = d.subsystem() + && sub == "scsi" + { + let s = d.sysname().to_string_lossy(); + if s.contains(':') { + hctl = Some(s.to_string()); + break; + } + } + current = d.parent(); + } + + if let Some(hctl_str) = hctl { + let sanitized = hctl_str.replace(':', "_"); + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("ScsiLed udev enumerator failed: {err}"); + PlatformError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("leds").map_err(|err| { + warn!("ScsiLed match_subsystem failed: {err}"); + PlatformError::Udev("match_subsystem failed".into(), err) + })?; + + for dev in enumerator.scan_devices().map_err(|err| { + warn!("ScsiLed scan_devices failed: {err}"); + PlatformError::Udev("scan_devices failed".into(), err) + })? { + let sysname = dev.sysname().to_string_lossy(); + let matches_name = sysname.contains("asus-scsi") + || sysname.contains("asus-aura-scsi") + || sysname.contains("asus-arion"); + let matches_hctl = sysname.contains(&sanitized) || sysname.contains(&hctl_str); + + if matches_name && matches_hctl { + info!( + "Found matched ASUS SCSI Dynamic Lighting LED at {:?}", + sysname + ); + let dynamic = DynamicLed::find(&sysname)?; + return Ok(Self { dynamic }); + } + } + } + + // Fallback: try finding any ASUS SCSI Dynamic Lighting LED + Self::new() + } + + /// Find a `ScsiLed` for a specific `/dev/sdX` or `/dev/sgN` path. + pub fn find_for_dev(dev_node: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|e| { + warn!("ScsiLed udev enumerator failed: {e}"); + PlatformError::Udev("enumerator failed".into(), e) + })?; + enumerator.match_subsystem("block").map_err(|e| { + warn!("ScsiLed match_subsystem failed: {e}"); + PlatformError::Udev("match block failed".into(), e) + })?; + + for dev in enumerator.scan_devices().map_err(|e| { + warn!("ScsiLed scan_devices failed: {e}"); + PlatformError::Udev("scan failed".into(), e) + })? { + if let Some(node) = dev.devnode() + && node.to_string_lossy() == dev_node + { + return Self::find_for_block(&dev); + } + } + + Self::new() + } + + /// Check if an ASUS SCSI Dynamic Lighting LED is available on the system. + pub fn is_available() -> bool { + Self::new().is_ok() + } + + /// Return reference to inner `DynamicLed`. + pub fn dynamic(&self) -> &DynamicLed { + &self.dynamic + } + + /// Return path to the sysfs node. + pub fn path(&self) -> &Path { + self.dynamic.path() + } + + /// Set animation effect string. + pub fn set_effect(&self, effect: &str) -> Result<()> { + self.dynamic.set_effect(effect) + } + + /// Set effect animation speed. + pub fn set_speed(&self, speed: u32) -> Result<()> { + self.dynamic.set_speed(speed) + } + + /// Set effect animation direction ("right" or "left"). + pub fn set_direction(&self, direction: &str) -> Result<()> { + self.dynamic.set_direction(direction) + } + + /// Set palette colors formatted as `(r, g, b)`. + pub fn set_palette_colors(&self, colors: &[(u8, u8, u8)]) -> Result<()> { + self.dynamic.set_palette_colors(colors) + } + + /// Write raw RGB bytes to the direct buffer. + pub fn write_direct(&self, data: &[u8]) -> Result<()> { + self.dynamic.write_direct(data) + } + + /// Set brightness (0..=255). + pub fn set_brightness(&self, brightness: u8) -> Result<()> { + self.dynamic.set_brightness(brightness) + } + + /// Get brightness (0..=255). + pub fn get_brightness(&self) -> Result { + self.dynamic.get_brightness() + } +} diff --git a/rog-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() - } -}