From 8d846e24bfd6dd12b792151549559562f4a88d04 Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 10:00:54 +0200 Subject: [PATCH 1/9] rog-platform: add HidRaw ioctl helpers and I2C-HID open (O_NONBLOCK) - Add HidrawDevinfo struct (8 bytes: __u32 bustype, __s16 vendor, __s16 product) matching the kernel struct exactly. - Add HIDIOCGRAWINFO / HIDIOCSFEATURE / HIDIOCGFEATURE ioctl number helpers. HIDIOCGRAWINFO uses size=8 (0x80084803), matching the kernel. - Add HidRaw::raw_info() -> HidrawDevinfo (reads VID/PID directly from hidraw, disambiguating LampArray-capable devices on I2C-HID where USB ATTRS are absent). - Add HidRaw::set_feature_report() and get_feature_report() for HID Feature reports (used by LampArray Control/Range/Attributes reports). - Add HidRaw::from_i2c_device() constructor that opens /dev/hidrawN in R/W with O_NONBLOCK. O_NONBLOCK defends against the i2c-hid kernel driver blocking during daemon init on some models (observed on ASUS TUF A16 FA608WV). - Add libc = "0.2" direct dependency (was transitively pulled in through udev already). All existing HidRaw call sites are unchanged. This is purely additive. --- Cargo.lock | 1 + rog-platform/Cargo.toml | 1 + rog-platform/src/hid_raw.rs | 214 ++++++++++++++++++++++++++++++++++-- 3 files changed, 208 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d9d73cc..d4dfbe2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4757,6 +4757,7 @@ version = "6.3.8" dependencies = [ "concat-idents", "inotify", + "libc", "log", "rusb", "serde", diff --git a/rog-platform/Cargo.toml b/rog-platform/Cargo.toml index ed5a3e01..7fbdcfbb 100644 --- a/rog-platform/Cargo.toml +++ b/rog-platform/Cargo.toml @@ -17,6 +17,7 @@ udev.workspace = true inotify.workspace = true rusb.workspace = true thiserror.workspace = true +libc = "0.2" [dev-dependencies] serde_json = { version = "1.0", default-features = false, features = ["preserve_order"] } diff --git a/rog-platform/src/hid_raw.rs b/rog-platform/src/hid_raw.rs index abb349c6..bb8e774e 100644 --- a/rog-platform/src/hid_raw.rs +++ b/rog-platform/src/hid_raw.rs @@ -1,13 +1,45 @@ use std::cell::RefCell; use std::fs::{File, OpenOptions}; use std::io::Write; +use std::os::unix::io::AsRawFd; +use std::os::unix::fs::OpenOptionsExt; use std::path::PathBuf; - -use log::{info, warn}; +use libc; +use log::{error, info, warn}; use udev::Device; - use crate::error::{PlatformError, Result}; +/// Matches the kernel `struct hidraw_devinfo` (8 bytes total). +#[repr(C)] +pub struct HidrawDevinfo { + pub bustype: u32, + pub vendor: i16, + pub product: i16, +} + +// ioctl number helpers (mirror the kernel macros _IOR / _IOWR) +const fn _ior(t: u32, nr: u32, size: u32) -> u32 { + (2u32 << 30) | (size << 16) | (t << 8) | nr +} +const fn _iowr(t: u32, nr: u32, size: u32) -> u32 { + (3u32 << 30) | (size << 16) | (t << 8) | nr +} + +/// HIDIOCGRAWINFO: returns the `hidraw_devinfo` struct (8 bytes -> 0x80084803). +pub fn hidiocgrawinfo() -> u32 { + _ior(b'H' as u32, 0x03, 8) +} + +/// HIDIOCSFEATURE(len) +pub fn hidiocsfeature(size: usize) -> u32 { + _iowr(b'H' as u32, 0x06, size as u32) +} + +/// HIDIOCGFEATURE(len) +pub fn hidiocgfeature(size: usize) -> u32 { + _iowr(b'H' as u32, 0x07, size as u32) +} + /// A USB device that utilizes hidraw for I/O #[derive(Debug)] pub struct HidRaw { @@ -21,19 +53,16 @@ pub struct HidRaw { /// Retaining a handle to the file for the duration of `HidRaw` file: RefCell, } - impl HidRaw { pub fn new(id_product: &str) -> Result { let mut enumerator = udev::Enumerator::new().map_err(|err| { warn!("{}", err); PlatformError::Udev("enumerator failed".into(), err) })?; - enumerator.match_subsystem("hidraw").map_err(|err| { warn!("{}", err); PlatformError::Udev("match_subsystem failed".into(), err) })?; - for endpoint in enumerator .scan_devices() .map_err(|e| PlatformError::IoPath("enumerator".to_owned(), e))? @@ -77,7 +106,6 @@ impl HidRaw { id_product ))) } - /// Make `HidRaw` device from a udev device pub fn from_device(endpoint: Device) -> Result { if let Some(parent) = endpoint @@ -108,10 +136,66 @@ impl HidRaw { )) } + /// Build a `HidRaw` from an I2C-HID hidraw endpoint. Opens R/W so that we + /// can use HIDIOCGFEATURE / HIDIOCSFEATURE on LampArray devices. + pub fn from_i2c_device(endpoint: Device, prod_id: &str) -> Result { + let sysname_dbg = endpoint + .sysname() + .to_string_lossy() + .to_string(); + info!( + "HidRaw::from_i2c_device: begin sysname={} prod_id={}", + sysname_dbg, prod_id + ); + info!("HidRaw::from_i2c_device: querying devnode for sysname={}", sysname_dbg); + let dev_node = endpoint.devnode().ok_or_else(|| { + PlatformError::MissingFunction("I2C-HID endpoint has no devnode".to_string()) + })?; + info!( + "HidRaw::from_i2c_device: devnode={:?} sysname={}", + dev_node, sysname_dbg + ); + info!( + "HidRaw::from_i2c_device: opening {:?} R/W (O_NONBLOCK) for prod_id={}", + dev_node, prod_id + ); + let file = OpenOptions::new() + .read(true) + .write(true) + .custom_flags(libc::O_NONBLOCK) + .open(dev_node) + .map_err(|e| PlatformError::IoPath(dev_node.to_string_lossy().to_string(), e))?; + let fd = file.as_raw_fd(); + info!( + "HidRaw::from_i2c_device: file opened fd={} dev_node={:?}", + fd, dev_node + ); + info!("HidRaw::from_i2c_device: about to query syspath for sysname={}", sysname_dbg); + let syspath = endpoint.syspath().to_path_buf(); + info!( + "HidRaw::from_i2c_device: syspath={:?} sysname={}", + syspath, sysname_dbg + ); + info!( + "HidRaw::from_i2c_device: returning OK for sysname={} fd={}", + sysname_dbg, fd + ); + Ok(Self { + file: RefCell::new(file), + devfs_path: dev_node.to_owned(), + prod_id: prod_id.to_string(), + syspath, + _device_bcd: 0, + }) + } + pub fn prod_id(&self) -> &str { &self.prod_id } + pub fn devfs_path(&self) -> &PathBuf { + &self.devfs_path + } /// 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() { @@ -122,11 +206,125 @@ impl HidRaw { } Ok(()) } - /// This method was added for certain devices like AniMe to prevent them /// waking the laptop pub fn set_wakeup_disabled(&self) -> Result<()> { let mut dev = Device::from_syspath(&self.syspath)?; Ok(dev.set_attribute_value("power/wakeup", "disabled")?) } + + /// HIDIOCGRAWINFO -> kernel hidraw_devinfo (bustype, vendor, product). + pub fn raw_info(&self) -> Result { + let file = self + .file + .try_borrow() + .map_err(|_| PlatformError::MissingFunction("hidraw file busy".into()))?; + let fd = file.as_raw_fd(); + let req = hidiocgrawinfo(); + let mut info = HidrawDevinfo { + bustype: 0, + vendor: 0, + product: 0, + }; + info!( + "HidRaw::raw_info: fd={} ioctl=0x{:08x} struct_size={}", + fd, + req, + std::mem::size_of::() + ); + // SAFETY: We pass a pointer to a 8-byte struct matching the kernel's + // hidraw_devinfo layout; the ioctl number encodes that size. + let ret = unsafe { + libc::ioctl( + fd, + req as libc::c_ulong, + &mut info as *mut HidrawDevinfo as *mut libc::c_void, + ) + }; + if ret < 0 { + let err = std::io::Error::last_os_error(); + error!( + "HidRaw::raw_info: ioctl HIDIOCGRAWINFO failed on {:?}: {}", + self.devfs_path, err + ); + return Err(PlatformError::IoPath( + self.devfs_path.to_string_lossy().to_string(), + err, + )); + } + info!( + "HidRaw::raw_info: ok bus={:#x} vendor={:#06x} product={:#06x}", + info.bustype, + info.vendor as u16, + info.product as u16 + ); + Ok(info) + } + + /// HIDIOCSFEATURE(len) - send a feature report. + pub fn set_feature_report(&self, payload: &[u8]) -> Result<()> { + let file = self + .file + .try_borrow() + .map_err(|_| PlatformError::MissingFunction("hidraw file busy".into()))?; + let fd = file.as_raw_fd(); + let req = hidiocsfeature(payload.len()); + // SAFETY: ioctl number encodes the buffer length, and we pass a pointer + // to a contiguous buffer of exactly that length. + let ret = unsafe { + libc::ioctl( + fd, + req as libc::c_ulong, + payload.as_ptr() as *mut libc::c_void, + ) + }; + if ret < 0 { + let err = std::io::Error::last_os_error(); + error!( + "HidRaw::set_feature_report: ioctl HIDIOCSFEATURE(len={}) failed on {:?}: {}", + payload.len(), + self.devfs_path, + err + ); + return Err(PlatformError::IoPath( + self.devfs_path.to_string_lossy().to_string(), + err, + )); + } + Ok(()) + } + + /// HIDIOCGFEATURE(len) - read a feature report. Buffer[0] must hold the + /// report ID before the call. + pub fn get_feature_report(&self, buf: &mut [u8]) -> Result { + let file = self + .file + .try_borrow() + .map_err(|_| PlatformError::MissingFunction("hidraw file busy".into()))?; + let fd = file.as_raw_fd(); + let req = hidiocgfeature(buf.len()); + // SAFETY: ioctl number encodes the buffer length, and we pass a pointer + // to a contiguous buffer of exactly that length. + let ret = unsafe { + libc::ioctl( + fd, + req as libc::c_ulong, + buf.as_mut_ptr() as *mut libc::c_void, + ) + }; + if ret < 0 { + let err = std::io::Error::last_os_error(); + error!( + "HidRaw::get_feature_report: ioctl HIDIOCGFEATURE(len={}) failed on {:?}: {}", + buf.len(), + self.devfs_path, + err + ); + return Err(PlatformError::IoPath( + self.devfs_path.to_string_lossy().to_string(), + err, + )); + } + Ok(ret as usize) + } } From f232dd58816a805b2620574d3595c633b0c2e32e Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 10:00:54 +0200 Subject: [PATCH 2/9] aura: add DeviceHandle::maybe_lamparray factory New async factory method on DeviceHandle that: 1. Reads VID/PID via HIDIOCGRAWINFO (removes any ambiguity from sysfs string formatting). 2. Whitelists ASUS vendor (0x0B05) and PID 0x19B6 (LampArray-capable keyboards on I2C-HID, first confirmed on ASUS TUF A16 FA608WV ITE5570). 3. Builds an Aura with is_lamparray=true and backlight=None (LampArray devices do not expose the legacy asus::kbd_backlight sysfs node). 4. Registers as AuraDeviceType::LaptopKeyboard2021 (which asusctl already maps for PID 0x19B6). The PID whitelist is intentionally narrow for the first PR. Adding more PIDs as users confirm support is a one-line change. --- asusd/src/aura_types.rs | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 6c255cfd..7ef91679 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -204,8 +204,62 @@ impl DeviceHandle { hid: device, backlight, config: Arc::new(Mutex::new(config)), + is_lamparray: false, }; aura.do_initialization().await?; Ok(Self::Aura(aura)) } + + /// Try the HID LampArray (Microsoft HID LampArray usage page) path used by + /// I2C-HID controllers on newer ASUS TUF laptops (e.g. FA608WV / ITE5570). + /// We open the hidraw R/W, run HIDIOCGRAWINFO and only proceed if the + /// device declares ASUS VID 0x0b05 and a whitelisted LampArray PID. + pub async fn maybe_lamparray( + device: Arc>, + prod_id: &str, + ) -> Result { + let dev_path = { + let g = device.lock().await; + g.devfs_path().clone() + }; + info!("LampArray init: device {dev_path:?} prod_id={prod_id:?}"); + info!("LampArray init: about to call raw_info on {dev_path:?}"); + let info_res = { + let g = device.lock().await; + g.raw_info() + }; + let info = match info_res { + Ok(i) => i, + Err(e) => { + error!("LampArray init: raw_info FAILED on {dev_path:?}: {e:?}"); + return Err(RogError::Platform(e)); + } + }; + let vid = (info.vendor as u16) as u32; + let pid = (info.product as u16) as u32; + info!( + "LampArray init: HIDIOCGRAWINFO {dev_path:?} VID:{vid:04x} PID:{pid:04x}" + ); + if vid != 0x0b05 { + return Err(RogError::NotFound(format!("Not ASUS: {vid:04x}"))); + } + if !matches!(pid, 0x19b6) { + return Err(RogError::NotFound(format!( + "PID {pid:04x} not on LampArray whitelist" + ))); + } + let aura_type = AuraDeviceType::from(prod_id); + info!("Found HID LampArray ASUS keyboard 0b05:{pid:04x}"); + let mut config = AuraConfig::load_and_update_config(prod_id); + config.led_type = aura_type; + let aura = Aura { + hid: Some(device), + backlight: None, + config: Arc::new(Mutex::new(config)), + is_lamparray: true, + }; + aura.do_initialization().await?; + info!("LampArray ready: 0b05:{pid:04x} on {dev_path:?}"); + Ok(Self::Aura(aura)) + } } From ae20c7e3bc8f203a49e2c15a91bacbde17643924 Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 10:00:54 +0200 Subject: [PATCH 3/9] aura: I2C-HID fallback discovery in init_hid_devices The existing init_hid_devices filters HID devices by USB parent only, silently skipping any device on I2C-HID. On ASUS 2024+ laptops the keyboard backlight controller is often an ITE chip on I2C-HID exposing the standard Microsoft HID LampArray usage page, and was never even probed. Changes: - When the USB-side path produces zero devices, fall through to a new init_i2c_hid_device helper. - init_i2c_hid_device walks the udev parent chain for the HID_ID property (format BUS:VENDOR:PRODUCT), parses VID/PID as numeric (BUG: previous drafts compared padded 8-char hex string against '0b05' and always failed), filters ASUS vendor, and opens the hidraw endpoint via HidRaw::from_i2c_device. - On success, calls DeviceHandle::maybe_lamparray and registers the device on the zbus server at /xyz/ljones/aura/lamparray_. - Every step logs at INFO level (Step1..Step6) with clear error reporting to make troubleshooting on new hardware straightforward. USB legacy path is entirely unchanged. --- asusd/src/aura_manager.rs | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/asusd/src/aura_manager.rs b/asusd/src/aura_manager.rs index 6d7fe524..da918fe6 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -127,6 +127,14 @@ impl DeviceManager { handles: Arc>>>>, ) -> Result, RogError> { let mut devices = Vec::new(); + let sysname_dbg = device.sysname().to_string_lossy().to_string(); + let devnode_dbg = device + .devnode() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "".to_string()); + info!( + "init_hid_devices: probing hidraw sysname={sysname_dbg} devnode={devnode_dbg}" + ); if let Some(usb_device) = device.parent_with_subsystem_devtype("usb", "usb_device")? { if let Some(usb_id) = usb_device.attribute_value("idProduct") { if let Some(vendor_id) = usb_device.attribute_value("idVendor") { @@ -206,6 +214,164 @@ impl DeviceManager { } } } + if devices.is_empty() { + info!( + "init_hid_devices: no USB-side device matched for {sysname_dbg}, trying I2C-HID fallback" + ); + match Self::init_i2c_hid_device(connection, &device, handles.clone()).await { + Ok(mut found) => { + info!( + "init_hid_devices: I2C-HID fallback for {sysname_dbg} returned {} device(s)", + found.len() + ); + devices.append(&mut found); + } + Err(e) => { + error!( + "init_hid_devices: I2C-HID fallback for {sysname_dbg} failed: {e:?}" + ); + } + } + } + Ok(devices) + } + + async fn init_i2c_hid_device( + connection: &Connection, + endpoint: &Device, + handles: Arc>>>>, + ) -> Result, RogError> { + let mut devices = Vec::new(); + let sysname = endpoint.sysname().to_string_lossy().to_string(); + info!("I2C-HID probe: examining hidraw {sysname}"); + let mut hid_id: Option = None; + let mut cur = endpoint.parent(); + while let Some(p) = cur { + if let Some(val) = p.property_value("HID_ID") { + hid_id = Some(val.to_string_lossy().to_string()); + break; + } + cur = p.parent(); + } + let Some(hid_id) = hid_id else { + info!("I2C-HID probe: no HID_ID property found for {sysname}"); + return Ok(devices); + }; + info!("I2C-HID probe: {sysname} HID_ID={hid_id}"); + let parts: Vec<&str> = hid_id.split(':').collect(); + if parts.len() != 3 { + info!("I2C-HID probe: HID_ID has unexpected shape: {hid_id}"); + return Ok(devices); + } + // The HID_ID format is "BUS:VENDOR:PRODUCT" where VENDOR and PRODUCT are + // 8-char hex strings (e.g. "0018:00000B05:000019B6"). We must parse them + // numerically — a naive string compare against "0B05" fails because the + // actual VID string is "00000B05". + let vendor_str = parts[1]; + let product_str = parts[2]; + let vendor = u32::from_str_radix(vendor_str, 16).unwrap_or(0xFFFF_FFFF); + let product = u32::from_str_radix(product_str, 16).unwrap_or(0xFFFF_FFFF); + info!( + "I2C-HID probe: VID parsing raw='{vendor_str}' parsed=0x{vendor:08x}; PID parsing raw='{product_str}' parsed=0x{product:08x}" + ); + if vendor != 0x0b05 { + info!( + "I2C-HID probe: not ASUS vendor 0x{vendor:08x} (raw '{vendor_str}'), skipping {sysname}" + ); + return Ok(devices); + } + let vid_upper = format!("{:04X}", vendor as u16); + let pid_upper = format!("{:04X}", product as u16); + let prod_id_str = pid_upper.to_lowercase(); + info!( + "Found ASUS HID LampArray candidate: {sysname} VID={vid_upper} PID={pid_upper}" + ); + info!("VID match: proceeding to open hidraw for {sysname}"); + info!("Step1: about to query devnode for {sysname}"); + let dev_node = match endpoint.devnode() { + Some(n) => n, + None => { + error!("I2C-HID probe: hidraw devnode missing for {sysname}"); + return Err(RogError::MissingFunction( + "I2C-HID hidraw devnode missing".to_string(), + )); + } + }; + info!("Step2: devnode={dev_node:?} for {sysname}"); + info!("I2C-HID probe: devnode={dev_node:?} for {sysname}"); + let key = dev_node.to_string_lossy().to_string(); + info!("Step3a: about to take handles map lock for cache lookup key={key}"); + let cached = handles.lock().await.get(&key).cloned(); + info!("Step3b: handles map lock released for {key} cached={}", cached.is_some()); + let handle = if let Some(existing) = cached { + info!("I2C-HID probe: reusing existing hidraw handle for {key}"); + existing + } else { + info!("Step3c: about to call HidRaw::from_i2c_device for {key} prod_id={prod_id_str}"); + info!("I2C-HID probe: creating HidRaw::from_i2c_device for {key} prod_id={prod_id_str}"); + // udev::Device contains a raw `*mut udev` and is !Send, so we + // cannot ship it into spawn_blocking. Instead we call the + // synchronous constructor here -- the actual blocking syscall is + // the OpenOptions::open in from_i2c_device, and we mitigate that + // separately by passing O_NONBLOCK there. + info!("Step3d: calling HidRaw::from_i2c_device synchronously for {key}"); + let hidraw_res = HidRaw::from_i2c_device(endpoint.clone(), &prod_id_str); + let hidraw = match hidraw_res { + Ok(h) => h, + Err(e) => { + error!( + "I2C-HID probe: HidRaw::from_i2c_device FAILED for {key}: {e:?}" + ); + return Err(e.into()); + } + }; + info!("Step4: hidraw handle created for {key}"); + info!("I2C-HID probe: HidRaw::from_i2c_device returned OK for {key}"); + let h = Arc::new(Mutex::new(hidraw)); + info!("Step4b: about to insert handle into handles map key={key}"); + handles.lock().await.insert(key.clone(), h.clone()); + info!("Step4c: handle inserted into handles map key={key}"); + h + }; + info!("Step5: about to call DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}"); + info!("Calling DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}"); + let result = DeviceHandle::maybe_lamparray(handle, &prod_id_str).await; + info!("Step6: maybe_lamparray returned for {sysname}"); + match result { + Ok(dev_type) => { + info!("maybe_lamparray OK for {sysname}"); + if let DeviceHandle::Aura(aura) = dev_type.clone() { + let path: OwnedObjectPath = ObjectPath::from_str_unchecked(&format!( + "{ASUS_ZBUS_PATH}/{MOD_NAME}/lamparray_{prod_id_str}" + )) + .into(); + info!("Registering LampArray device on zbus at path={path:?}"); + let ctrl = AuraZbus::new(aura); + match ctrl.start_tasks(connection, path.clone()).await { + Ok(_) => info!("LampArray zbus start_tasks OK for {path:?}"), + Err(e) => { + error!( + "LampArray zbus start_tasks FAILED for {path:?}: {e:?}" + ); + return Ok(devices); + } + } + devices.push(AsusDevice { + device: dev_type, + dbus_path: path.clone(), + hid_key: Some(key), + }); + info!("LampArray device added to manager at {path:?}"); + } else { + info!( + "maybe_lamparray returned non-Aura variant for {sysname}, ignoring" + ); + } + } + Err(e) => { + error!("maybe_lamparray FAILED for {sysname}: {e:?}"); + } + } Ok(devices) } From d7db53f77fab843048ae0e226d4f3ca431c6046d Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 10:00:54 +0200 Subject: [PATCH 4/9] aura: implement LampArray write path in Aura + trait_impls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the write side of the LampArray support. aura_laptop::mod.rs: - Add is_lamparray: bool field to Aura struct. - Add lamparray_push_rgb_i() helper: disables autonomous mode (HIDIOCSFEATURE [0x46, 0x00]) then sends LampRangeUpdate (HIDIOCSFEATURE [0x45, 0x01, LampIdStart(2 LE), LampIdEnd(2 LE), R, G, B, I]) covering the whole LampCount range. - Add lamparray_write_effect(&self, mode: &AuraEffect) — non-locking variant used when the caller does not hold the config lock. Uses try_lock with a Med intensity fallback to avoid deadlock. - Add lamparray_write_effect_locked(&self, config: &AuraConfig, mode: &AuraEffect) — called from write_current_config_mode which already owns the config lock, avoiding the double-lock deadlock. - Add lamparray_set_brightness() and lamparray_set_aura_power() with the same lock-safe pattern. - write_effect_and_apply short-circuits into the LampArray branch when is_lamparray is true. aura_laptop::trait_impls.rs: - Route set_led_mode / set_led_mode_data through lamparray_write_effect_locked when is_lamparray is true. - Skip the redundant set_brightness call for LampArray devices (brightness is encoded in the intensity byte of range update; the legacy path did a separate call that on LampArray fell back to white and overwrote the color). - Every zbus method that previously returned MissingFunction( "No LED backlight control available") when backlight was None now early-returns into the LampArray branch when is_lamparray is true. USB legacy path unchanged (branches are gated on self.is_lamparray). --- asusd/src/aura_laptop/mod.rs | 205 ++++++++++++++++++++++++++- asusd/src/aura_laptop/trait_impls.rs | 42 +++++- 2 files changed, 237 insertions(+), 10 deletions(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index bc053df6..9979d957 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -20,6 +20,11 @@ pub struct Aura { pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, + /// True when this Aura is driven via the HID LampArray usage page + /// (I2C-HID controllers on newer ASUS TUF laptops). When true the + /// asus-wmi sysfs backlight is absent and all interactions go through + /// HIDIOC[SG]FEATURE feature reports. + pub is_lamparray: bool, } impl Aura { @@ -36,7 +41,10 @@ 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 self.is_lamparray { + // LampArray brightness lives entirely in our config (no sysfs node). + config.brightness.into() + } else if let Some(bl) = self.backlight.as_ref() { bl.lock().await.get_brightness().unwrap_or_default() } else { config.brightness.into() @@ -69,15 +77,26 @@ impl Aura { if let Some(multizones) = config.multizone.as_mut() { if let Some(set) = multizones.get(&mode) { for mode in set.clone() { - self.write_effect_and_apply(config.led_type, &mode).await?; + if self.is_lamparray { + // Caller already owns `config`; re-locking + // `self.config` inside `lamparray_write_effect` + // used to deadlock us at init time. + self.lamparray_write_effect_locked(config, &mode).await?; + } else { + self.write_effect_and_apply(config.led_type, &mode).await?; + } } } } } else { let mode = config.current_mode; if let Some(effect) = config.builtins.get(&mode).cloned() { - self.write_effect_and_apply(config.led_type, &effect) - .await?; + if self.is_lamparray { + self.lamparray_write_effect_locked(config, &effect).await?; + } else { + self.write_effect_and_apply(config.led_type, &effect) + .await?; + } } } @@ -93,6 +112,13 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { + if self.is_lamparray { + info!( + "LampArray write_effect_and_apply: dev_type={:?} mode={:?}", + dev_type, mode.mode + ); + return self.lamparray_write_effect(mode).await; + } if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { if let Some(platform) = &self.backlight { let buf = [ @@ -128,6 +154,9 @@ impl Aura { } pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + if self.is_lamparray { + return self.lamparray_set_brightness(value).await; + } if let Some(backlight) = &self.backlight { backlight.lock().await.set_brightness(value)?; return Ok(()); @@ -140,6 +169,9 @@ 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 self.is_lamparray { + return self.lamparray_set_aura_power(config).await; + } if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { if let Some(backlight) = &self.backlight { // TODO: tuf bool array @@ -238,4 +270,169 @@ impl Aura { } Ok(()) } + + /// LampArray helper - write the current static colour to the whole + /// keyboard at the requested intensity (0-255). The protocol is the + /// Microsoft HID LampArray usage page: + /// * report 0x46 - "autonomous mode" toggle (we disable so the OS owns) + /// * report 0x41 - LampArrayAttributes (read to discover LampCount) + /// * report 0x45 - LampArrayMultiUpdate / RangeUpdate + async fn lamparray_push_rgb_i( + &self, + r: u8, + g: u8, + b: u8, + intensity: u8, + ) -> Result<(), RogError> { + let hid_arc = self + .hid + .as_ref() + .ok_or(RogError::NoAuraKeyboard)? + .clone(); + let hid = hid_arc.lock().await; + // Disable autonomous so we own the lamp array + hid.set_feature_report(&[0x46, 0x00])?; + // Read LampArrayAttributes to discover the lamp count + let mut attr = vec![0u8; 23]; + attr[0] = 0x41; + hid.get_feature_report(&mut attr)?; + let lamp_count = u16::from_le_bytes([attr[1], attr[2]]); + if lamp_count == 0 { + return Err(RogError::MissingFunction( + "LampArray reports zero lamps".to_string(), + )); + } + let last = lamp_count - 1; + // RangeUpdate: 0x45, flags, start_lo, start_hi, end_lo, end_hi, r,g,b,i + let payload = [ + 0x45, + 0x01, + 0x00, + 0x00, + (last & 0xff) as u8, + ((last >> 8) & 0xff) as u8, + r, + g, + b, + intensity, + ]; + hid.set_feature_report(&payload)?; + info!( + "LampArray ready: LampCount={lamp_count} rgb=({r:02x},{g:02x},{b:02x}) i={intensity}" + ); + Ok(()) + } + + /// Write a single effect (static colour for now) to a LampArray device. + /// + /// IMPORTANT: this used to take `self.config.lock().await` to read + /// brightness, but the typical call chain comes from a caller that ALREADY + /// holds the config lock (e.g. `write_current_config_mode`, + /// `set_led_mode_data`, `reload`). Re-locking caused an async deadlock at + /// init time, which made systemd kill asusd on the `Type=dbus` timeout. + /// We now use `try_lock` and fall back to `LedBrightness::Med` when the + /// lock is held by the caller. Callers that already have a locked + /// `AuraConfig` should prefer `lamparray_write_effect_locked` to avoid + /// the fallback path entirely. + pub async fn lamparray_write_effect(&self, mode: &AuraEffect) -> Result<(), RogError> { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + let brightness = match self.config.try_lock() { + Ok(cfg) => cfg.brightness, + Err(_) => { + info!( + "lamparray_write_effect: config already locked by caller, using Med fallback" + ); + LedBrightness::Med + } + }; + let intensity = Self::brightness_to_intensity(brightness); + info!("lamparray_write_effect: about to push rgb (no lock held)"); + self.lamparray_push_rgb_i(r, g, b, intensity).await + } + + /// Variant for callers that already hold the config lock. Pass the + /// already-locked config in to avoid the deadlock that re-locking would + /// cause. + pub async fn lamparray_write_effect_locked( + &self, + config: &AuraConfig, + mode: &AuraEffect, + ) -> Result<(), RogError> { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + let intensity = Self::brightness_to_intensity(config.brightness); + info!("lamparray_write_effect_locked: about to push rgb (caller owns config lock)"); + self.lamparray_push_rgb_i(r, g, b, intensity).await + } + + fn brightness_to_intensity(b: LedBrightness) -> u8 { + match b { + LedBrightness::Off => 0, + LedBrightness::Low => 64, + LedBrightness::Med => 128, + LedBrightness::High => 255, + } + } + + /// Brightness -> intensity mapping for LampArray. Reuses the colour from + /// the currently active builtin effect in config so the keyboard keeps + /// the same hue when the user only changes brightness. + /// + /// Uses `try_lock` to avoid the init-time deadlock when a caller higher + /// in the stack already owns the config lock (see comment on + /// `lamparray_write_effect`). + pub async fn lamparray_set_brightness(&self, value: u8) -> Result<(), RogError> { + let level = match value { + 0 => LedBrightness::Off, + 1 => LedBrightness::Low, + 2 => LedBrightness::Med, + _ => LedBrightness::High, + }; + let intensity = Self::brightness_to_intensity(level); + let (r, g, b) = match self.config.try_lock() { + Ok(mut cfg) => { + cfg.brightness = level; + let mode = cfg.current_mode; + if let Some(eff) = cfg.builtins.get(&mode) { + (eff.colour1.r, eff.colour1.g, eff.colour1.b) + } else { + (0xff, 0xff, 0xff) + } + } + Err(_) => { + info!( + "lamparray_set_brightness: config already locked by caller, defaulting to white" + ); + (0xff, 0xff, 0xff) + } + }; + info!("lamparray_set_brightness: about to push rgb (no lock held)"); + self.lamparray_push_rgb_i(r, g, b, intensity).await + } + + /// Aura power states on LampArray - we collapse the per-zone flags into a + /// simple on/off: any zone enabled -> full intensity with the saved RGB, + /// all disabled -> intensity 0. + pub async fn lamparray_set_aura_power( + &self, + config: &AuraConfig, + ) -> Result<(), RogError> { + let any_on = config.enabled.states.iter().any(|s| { + // Treat the "new" zone state as on if any bit is set. + s.new_to_byte() != 0 + }); + let (r, g, b) = { + let mode = config.current_mode; + if let Some(eff) = config.builtins.get(&mode) { + (eff.colour1.r, eff.colour1.g, eff.colour1.b) + } else { + (0xff, 0xff, 0xff) + } + }; + let intensity = if any_on { 255 } else { 0 }; + self.lamparray_push_rgb_i(r, g, b, intensity).await + } } diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 8a4daa66..d5159cc7 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -61,6 +61,9 @@ impl AuraZbus { /// Return the current LED brightness #[zbus(property)] async fn brightness(&self) -> Result { + if self.0.is_lamparray { + return Ok(self.0.config.lock().await.brightness); + } if let Some(bl) = self.0.backlight.as_ref() { return Ok(bl.lock().await.get_brightness().map(|n| n.into())?); } @@ -70,6 +73,13 @@ impl AuraZbus { /// Set the keyboard brightness level (0-3) #[zbus(property)] async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { + if self.0.is_lamparray { + self.0.lamparray_set_brightness(brightness.into()).await?; + let mut config = self.0.config.lock().await; + config.brightness = brightness; + config.write(); + return Ok(()); + } if let Some(bl) = self.0.backlight.as_ref() { let res = bl.lock().await.set_brightness(brightness.into()); if res.is_ok() { @@ -133,11 +143,17 @@ impl AuraZbus { async fn set_led_mode(&mut self, num: AuraModeNum) -> Result<(), ZbErr> { let mut config = self.0.config.lock().await; config.current_mode = num; - self.0.write_current_config_mode(&mut config).await?; if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - self.0.set_brightness(config.brightness.into()).await?; + self.0.write_current_config_mode(&mut config).await?; + if !self.0.is_lamparray { + // For LampArray write_current_config_mode already pushed both + // colour and intensity in one HID feature report (via the + // *_locked path). Avoid a second push that would race the + // colour with the white fallback in lamparray_set_brightness. + self.0.set_brightness(config.brightness.into()).await?; + } config.write(); Ok(()) } @@ -173,13 +189,27 @@ impl AuraZbus { ))); } - self.0 - .write_effect_and_apply(config.led_type, &effect) - .await?; if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - self.0.set_brightness(config.brightness.into()).await?; + if self.0.is_lamparray { + // LampArray: a single HID feature report carries both colour and + // intensity, so we must push them together. The caller already + // owns config.lock(), so use the *_locked variant to avoid the + // try_lock fallback in lamparray_write_effect that would clobber + // the colour with a Med/white default. We also skip the + // subsequent set_brightness() call because lamparray_set_brightness + // would race the colour we just wrote (try_lock fails -> white + // fallback push). + self.0 + .lamparray_write_effect_locked(&config, &effect) + .await?; + } else { + self.0 + .write_effect_and_apply(config.led_type, &effect) + .await?; + self.0.set_brightness(config.brightness.into()).await?; + } config.set_builtin(effect); config.write(); Ok(()) From 489b31479f1a0efe171ea455a7c1250bc9c5f4aa Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 10:00:54 +0200 Subject: [PATCH 5/9] daemon: register dbus name before device discovery With Type=dbus and TimeoutSec=10, systemd considers the service ready only when the name xyz.ljones.Asusd appears on the bus. If DeviceManager::new blocks for any reason (a slow HID init on a flaky controller, an initial discovery over many hidraw endpoints), the name never gets registered in time and systemd kills the process. Fix: call server.request_name(DBUS_NAME) BEFORE DeviceManager::new, and wrap DeviceManager::new in a non-fatal match (errors logged, but the daemon keeps running so users can still get platform / fancurves / armoury interfaces even if HID discovery fails). The zbus server accepts incoming method calls after request_name regardless of whether DeviceManager has finished. --- asusd/src/daemon.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/asusd/src/daemon.rs b/asusd/src/daemon.rs index 64f2da99..d3c7d2a9 100644 --- a/asusd/src/daemon.rs +++ b/asusd/src/daemon.rs @@ -150,10 +150,6 @@ async fn start_daemon() -> Result<(), Box> { } } - let _ = DeviceManager::new(server.clone()).await?; - - info!("DeviceManager initialized"); - // XG Mobile LED (non-fatal if not attached) match CtrlXgmLed::try_new(config.clone()) { Ok(Some(ctrl)) => { @@ -179,10 +175,29 @@ async fn start_daemon() -> Result<(), Box> { info!("CtrlGpu: initialized"); } - // Request dbus name after finishing initalizing all functions + // Request the well-known dbus name BEFORE we go enumerate hidraw nodes. + // systemd's `Type=dbus` ready signal is the appearance of this name on + // the bus; if we delay it behind device probing (which on FA608WV + // includes a LampArray HID conversation that can stall on a kernel + // hidraw read), systemd kills us with a TimeoutSec=10 timeout and the + // service ends up reported as ServiceUnknown / "not activatable". + // We can still register additional zbus objects after request_name; the + // object server accepts new paths at any time. server.request_name(DBUS_NAME).await?; - info!("Startup success on dbus name {DBUS_NAME}: begining dbus server loop"); + info!("Startup ready on dbus name {DBUS_NAME}: hidraw enumeration runs in background"); + + // Now do the hidraw enumeration. The well-known name is already on the + // bus so systemd considers us ready, and if a device probe stalls it + // cannot trigger the systemd start timeout. (We can't easily push this + // off into a `tokio::spawn` because `udev::Enumerator` and friends are + // not `Send`.) + match DeviceManager::new(server.clone()).await { + Ok(_) => info!("DeviceManager initialized"), + Err(e) => error!("DeviceManager init failed: {e:?}"), + } + + info!("Entering dbus server loop"); loop { // This is just a blocker to idle and ensure the reator reacts server.executor().tick().await; From 81ec0e2c4e1d4bd85471a1eaa0c03ef18b8f32ea Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 15:23:34 +0200 Subject: [PATCH 6/9] aura: implement LampArray dynamic effects (Breathe/RainbowCycle/RainbowWave/Pulse) - Add effect_task field on Aura for tokio JoinHandle tracking - Add lamparray_stop_effect_task/lamparray_spawn_effect helpers - Route write_effect and write_effect_locked to spawn_effect for non-Static modes - Probe LampCount once before task start so animation loop never touches GET_FEATURE at 30 FPS - Implement 30 FPS Breathe (sinusoidal I), Pulse (asymmetric spike), RainbowCycle (HSV hue rotation), RainbowWave (reverse HSV rotation on LampCount=1 where there is no spatial wave) - Speed enum mapped to ~4s / ~2s / ~0.8s period for Low/Med/High - Task is aborted before spawning new effect and before power-state changes (no overlapping frames) - Task releases hid lock between frames so brightness/other callers can interleave --- asusd/src/aura_laptop/mod.rs | 256 +++++++++++++++++++++++++++++++++-- asusd/src/aura_types.rs | 2 + 2 files changed, 247 insertions(+), 11 deletions(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 9979d957..994dda50 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -1,14 +1,16 @@ use std::sync::Arc; +use std::time::Duration; 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::{AuraDeviceType, AuraEffect, LedBrightness, PowerZones, AURA_LAPTOP_LED_MSG_LEN}; +use rog_aura::{AuraDeviceType, AuraEffect, AuraModeNum, LedBrightness, PowerZones, Speed, AURA_LAPTOP_LED_MSG_LEN}; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; use tokio::sync::{Mutex, MutexGuard}; +use tokio::task::JoinHandle; use crate::error::RogError; @@ -25,6 +27,13 @@ pub struct Aura { /// asus-wmi sysfs backlight is absent and all interactions go through /// HIDIOC[SG]FEATURE feature reports. pub is_lamparray: bool, + /// Handle for the currently-running LampArray dynamic-effect task. + /// The LampArray chip is passive: animations (Breathe, RainbowCycle, + /// RainbowWave, Pulse, ...) are driven by the host pushing + /// LampRangeUpdate frames at ~30 FPS from a tokio task. When a new + /// effect is written we abort the old task first so we never have two + /// loops fighting over the hid lock. + pub effect_task: Arc>>>, } impl Aura { @@ -335,9 +344,9 @@ impl Aura { /// `AuraConfig` should prefer `lamparray_write_effect_locked` to avoid /// the fallback path entirely. pub async fn lamparray_write_effect(&self, mode: &AuraEffect) -> Result<(), RogError> { - let r = mode.colour1.r; - let g = mode.colour1.g; - let b = mode.colour1.b; + // Always stop any previous animation loop before doing anything else, + // so two effect tasks never race to push frames. + self.lamparray_stop_effect_task().await; let brightness = match self.config.try_lock() { Ok(cfg) => cfg.brightness, Err(_) => { @@ -348,8 +357,22 @@ impl Aura { } }; let intensity = Self::brightness_to_intensity(brightness); - info!("lamparray_write_effect: about to push rgb (no lock held)"); - self.lamparray_push_rgb_i(r, g, b, intensity).await + match mode.mode { + AuraModeNum::Static => { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + info!("lamparray_write_effect: Static, single push"); + self.lamparray_push_rgb_i(r, g, b, intensity).await + } + _ => { + info!( + "lamparray_write_effect: dynamic mode {:?}, spawning effect task", + mode.mode + ); + self.lamparray_spawn_effect(mode.clone(), intensity).await + } + } } /// Variant for callers that already hold the config lock. Pass the @@ -360,12 +383,26 @@ impl Aura { config: &AuraConfig, mode: &AuraEffect, ) -> Result<(), RogError> { - let r = mode.colour1.r; - let g = mode.colour1.g; - let b = mode.colour1.b; + // Same rule as `lamparray_write_effect`: kill any running animation + // before dispatching so we don't accumulate tasks across reloads. + self.lamparray_stop_effect_task().await; let intensity = Self::brightness_to_intensity(config.brightness); - info!("lamparray_write_effect_locked: about to push rgb (caller owns config lock)"); - self.lamparray_push_rgb_i(r, g, b, intensity).await + match mode.mode { + AuraModeNum::Static => { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + info!("lamparray_write_effect_locked: Static, single push"); + self.lamparray_push_rgb_i(r, g, b, intensity).await + } + _ => { + info!( + "lamparray_write_effect_locked: dynamic mode {:?}, spawning effect task", + mode.mode + ); + self.lamparray_spawn_effect(mode.clone(), intensity).await + } + } } fn brightness_to_intensity(b: LedBrightness) -> u8 { @@ -433,6 +470,203 @@ impl Aura { } }; let intensity = if any_on { 255 } else { 0 }; + // A power-state change also implies "stop whatever animation was + // running", otherwise the loop would happily override our push. + self.lamparray_stop_effect_task().await; self.lamparray_push_rgb_i(r, g, b, intensity).await } + + /// Abort the current LampArray effect task, if any. Safe to call even + /// when no task is running. + pub async fn lamparray_stop_effect_task(&self) { + let mut slot = self.effect_task.lock().await; + if let Some(handle) = slot.take() { + handle.abort(); + info!("lamparray_effect_task: cancelled"); + } + } + + /// Spawn a tokio task that drives one of the dynamic LampArray effects + /// (Breathe / RainbowCycle / RainbowWave / Pulse). The task loops at + /// ~30 FPS pushing LampRangeUpdate feature reports. `intensity` is the + /// current brightness cap (0..=255): brightness-driven effects + /// (Breathe, Pulse) modulate I within this cap; HSV effects + /// (RainbowCycle, RainbowWave) pass it straight through. + async fn lamparray_spawn_effect( + &self, + mode: AuraEffect, + intensity: u8, + ) -> Result<(), RogError> { + let hid_arc = self + .hid + .as_ref() + .ok_or(RogError::NoAuraKeyboard)? + .clone(); + + // Probe LampCount once, up-front, so the task doesn't need to touch + // GET_FEATURE at 30 FPS. + let lamp_count = { + let hid = hid_arc.lock().await; + hid.set_feature_report(&[0x46, 0x00])?; + let mut attr = vec![0u8; 23]; + attr[0] = 0x41; + hid.get_feature_report(&mut attr)?; + u16::from_le_bytes([attr[1], attr[2]]) + }; + if lamp_count == 0 { + return Err(RogError::MissingFunction( + "LampArray reports zero lamps".to_string(), + )); + } + + let period_ms = Self::speed_to_period_ms(mode.speed); + let frame_ms: u64 = 33; // ~30 FPS + let total_frames: u32 = + ((period_ms as f32) / (frame_ms as f32)).max(1.0) as u32; + let mode_kind = mode.mode; + let colour1 = mode.colour1; + + info!( + "lamparray_effect_task: starting mode={:?} period={}ms frames={} rgb1=({},{},{}) intensity_cap={}", + mode_kind, + period_ms, + total_frames, + colour1.r, + colour1.g, + colour1.b, + intensity + ); + + let hid_for_task = hid_arc.clone(); + let handle = tokio::spawn(async move { + let mut ticker = + tokio::time::interval(Duration::from_millis(frame_ms)); + // Discard the immediate first tick so the loop pacing is stable. + ticker.tick().await; + let mut frame: u32 = 0; + loop { + let t = (frame % total_frames) as f32 + / (total_frames as f32); + let (r, g, b, i) = match mode_kind { + AuraModeNum::Breathe => { + // Pure sinusoid on I; keep colour1 as the hue. + let s = (2.0 + * std::f32::consts::PI + * t) + .sin(); + let level = ((s + 1.0) * 0.5) * intensity as f32; + ( + colour1.r, + colour1.g, + colour1.b, + level.round().clamp(0.0, 255.0) as u8, + ) + } + AuraModeNum::Pulse => { + // Sharp attack, slow decay - "heartbeat" style. + let phase = t; + let level = if phase < 0.2 { + (phase / 0.2) * intensity as f32 + } else { + (1.0 - (phase - 0.2) / 0.8) * intensity as f32 + }; + ( + colour1.r, + colour1.g, + colour1.b, + level.round().clamp(0.0, 255.0) as u8, + ) + } + AuraModeNum::RainbowCycle => { + let hue = (t * 360.0) % 360.0; + let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); + (r, g, b, intensity) + } + AuraModeNum::RainbowWave => { + // On LampCount=1 there is no spatial "wave" to + // encode - a single lamp is scalar. We keep the + // same hue rotation as RainbowCycle but sweep the + // hue backwards to give the user a visual + // difference between the two modes. + let hue = (360.0 - (t * 360.0)) % 360.0; + let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); + (r, g, b, intensity) + } + // Should not happen: Static and unhandled modes go + // through the single-push path in write_effect. + _ => (colour1.r, colour1.g, colour1.b, intensity), + }; + + let last = lamp_count - 1; + let payload = [ + 0x45, + 0x01, + 0x00, + 0x00, + (last & 0xff) as u8, + ((last >> 8) & 0xff) as u8, + r, + g, + b, + i, + ]; + // Hold the hid lock only for the write, so brightness/other + // callers can interleave between frames. + { + let hid = hid_for_task.lock().await; + if let Err(e) = hid.set_feature_report(&payload) { + log::warn!( + "lamparray_effect_task: set_feature_report failed: {e:?} - stopping" + ); + break; + } + } + + frame = frame.wrapping_add(1); + ticker.tick().await; + } + info!("lamparray_effect_task: exited"); + }); + + let mut slot = self.effect_task.lock().await; + *slot = Some(handle); + Ok(()) + } + + /// Map the abstract rog_aura::Speed enum to a period in milliseconds + /// for one full cycle of the animation. + fn speed_to_period_ms(s: Speed) -> u32 { + match s { + Speed::Low => 4000, + Speed::Med => 2000, + Speed::High => 800, + } + } +} + +/// Convert an HSV colour (hue in degrees, s/v in [0, 1]) to 8-bit RGB. +/// Standard formula from https://en.wikipedia.org/wiki/HSL_and_HSV. +fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { + let c = v * s; + let hp = (h / 60.0) % 6.0; + let x = c * (1.0 - ((hp % 2.0) - 1.0).abs()); + let (r1, g1, b1) = if hp < 1.0 { + (c, x, 0.0) + } else if hp < 2.0 { + (x, c, 0.0) + } else if hp < 3.0 { + (0.0, c, x) + } else if hp < 4.0 { + (0.0, x, c) + } else if hp < 5.0 { + (x, 0.0, c) + } else { + (c, 0.0, x) + }; + let m = v - c; + ( + ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ) } diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 7ef91679..af877c03 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -205,6 +205,7 @@ impl DeviceHandle { backlight, config: Arc::new(Mutex::new(config)), is_lamparray: false, + effect_task: Arc::new(Mutex::new(None)), }; aura.do_initialization().await?; Ok(Self::Aura(aura)) @@ -257,6 +258,7 @@ impl DeviceHandle { backlight: None, config: Arc::new(Mutex::new(config)), is_lamparray: true, + effect_task: Arc::new(Mutex::new(None)), }; aura.do_initialization().await?; info!("LampArray ready: 0b05:{pid:04x} on {dev_path:?}"); From 802b9d4b43cd6d13ebf1606bf17f3a66e87be4a2 Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 15:31:30 +0200 Subject: [PATCH 7/9] aura: fix tokio spawn panic when invoked from zbus executor thread The dynamic-effect spawn path used bare tokio::spawn() which panics when called from the zbus Connection executor thread (not a tokio runtime thread). Capture tokio::runtime::Handle at Aura construction (guaranteed tokio context via maybe_lamparray async fn) and use Handle::spawn() which works from any thread. Fixes core-dump on any 'asusctl aura effect breathe/rainbow-cycle/ rainbow-wave/pulse' invocation. --- asusd/src/aura_laptop/mod.rs | 8 +++++++- asusd/src/aura_types.rs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 994dda50..2254bd23 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -9,6 +9,7 @@ use rog_aura::usb::{AURA_LAPTOP_LED_APPLY, AURA_LAPTOP_LED_SET}; use rog_aura::{AuraDeviceType, AuraEffect, AuraModeNum, LedBrightness, PowerZones, Speed, AURA_LAPTOP_LED_MSG_LEN}; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; +use tokio::runtime::Handle; use tokio::sync::{Mutex, MutexGuard}; use tokio::task::JoinHandle; @@ -34,6 +35,11 @@ pub struct Aura { /// effect is written we abort the old task first so we never have two /// loops fighting over the hid lock. pub effect_task: Arc>>>, + /// Tokio runtime handle captured at construction. Used to spawn the + /// dynamic-effect task from methods invoked on the zbus executor thread + /// (which is *not* a Tokio runtime thread). `Handle::spawn()` works from + /// any thread; bare `tokio::spawn()` would panic there. + pub runtime_handle: Handle, } impl Aura { @@ -538,7 +544,7 @@ impl Aura { ); let hid_for_task = hid_arc.clone(); - let handle = tokio::spawn(async move { + let handle = self.runtime_handle.spawn(async move { let mut ticker = tokio::time::interval(Duration::from_millis(frame_ms)); // Discard the immediate first tick so the loop pacing is stable. diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index af877c03..9951b85b 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -206,6 +206,7 @@ impl DeviceHandle { config: Arc::new(Mutex::new(config)), is_lamparray: false, effect_task: Arc::new(Mutex::new(None)), + runtime_handle: tokio::runtime::Handle::current(), }; aura.do_initialization().await?; Ok(Self::Aura(aura)) @@ -259,6 +260,7 @@ impl DeviceHandle { config: Arc::new(Mutex::new(config)), is_lamparray: true, effect_task: Arc::new(Mutex::new(None)), + runtime_handle: tokio::runtime::Handle::current(), }; aura.do_initialization().await?; info!("LampArray ready: 0b05:{pid:04x} on {dev_path:?}"); From 73058b6f0f1cf36e924f23707bf90d705a36f830 Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 16:17:19 +0200 Subject: [PATCH 8/9] aura: extract LampArray into dedicated aura_lamparray module Move all LampArray-specific code out of aura_laptop into its own module sibling to aura_slash / aura_anime / aura_scsi. This is a cleaner mapping of the actual protocol split: LampArray is HID Usage Page 0x59 standard, distinct from the ASUS proprietary aura HID protocol used by USB keyboards. - New: asusd/src/aura_lamparray/{mod.rs, effects.rs, trait_impls.rs} - New: DeviceHandle::LampArray(LampArray) variant in aura_types.rs - Removed: Aura::is_lamparray flag and all branching from aura_laptop - Behavior unchanged: same D-Bus path, same commands, same protocol --- asusd/src/aura_lamparray/effects.rs | 183 ++++++++++ asusd/src/aura_lamparray/mod.rs | 353 ++++++++++++++++++ asusd/src/aura_lamparray/trait_impls.rs | 281 +++++++++++++++ asusd/src/aura_laptop/mod.rs | 459 +----------------------- asusd/src/aura_laptop/trait_impls.rs | 67 +--- asusd/src/aura_manager.rs | 119 +++--- asusd/src/aura_types.rs | 57 +-- asusd/src/lib.rs | 1 + 8 files changed, 911 insertions(+), 609 deletions(-) create mode 100644 asusd/src/aura_lamparray/effects.rs create mode 100644 asusd/src/aura_lamparray/mod.rs create mode 100644 asusd/src/aura_lamparray/trait_impls.rs diff --git a/asusd/src/aura_lamparray/effects.rs b/asusd/src/aura_lamparray/effects.rs new file mode 100644 index 00000000..96f712a2 --- /dev/null +++ b/asusd/src/aura_lamparray/effects.rs @@ -0,0 +1,183 @@ +//! LampArray dynamic effects: frame generation for +//! Breathe / RainbowCycle / RainbowWave / Pulse. +//! +//! Split out from `mod.rs` so the animation math stays isolated from the +//! outer `LampArray` bookkeeping (config lock, effect_task handle). +use std::sync::Arc; +use std::time::Duration; + +use log::info; +use rog_aura::{AuraEffect, AuraModeNum, Speed}; +use rog_platform::hid_raw::HidRaw; +use tokio::runtime::Handle; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; + +use crate::error::RogError; + +/// info! only in debug builds — release stays quiet. +macro_rules! debug_info { + ($($arg:tt)*) => {{ + if cfg!(debug_assertions) { + log::info!($($arg)*); + } + }}; +} + +/// Spawn the animation task. Probes LampCount once (blocking-ish, but only +/// runs on effect switch), then loops at ~30 FPS pushing LampRangeUpdate +/// feature reports until aborted. +pub async fn spawn_effect_task( + hid: Arc>, + runtime_handle: &Handle, + mode: AuraEffect, + intensity: u8, +) -> Result, RogError> { + // Probe LampCount once, up-front, so the task doesn't need to touch + // GET_FEATURE at 30 FPS. + let lamp_count = { + let hid = hid.lock().await; + hid.set_feature_report(&[0x46, 0x00])?; + let mut attr = vec![0u8; 23]; + attr[0] = 0x41; + hid.get_feature_report(&mut attr)?; + u16::from_le_bytes([attr[1], attr[2]]) + }; + if lamp_count == 0 { + return Err(RogError::MissingFunction( + "LampArray reports zero lamps".to_string(), + )); + } + let period_ms = speed_to_period_ms(mode.speed); + let frame_ms: u64 = 33; // ~30 FPS + let total_frames: u32 = + ((period_ms as f32) / (frame_ms as f32)).max(1.0) as u32; + let mode_kind = mode.mode; + let colour1 = mode.colour1; + info!( + "lamparray_effect_task: starting mode={:?} period={}ms frames={} rgb1=({},{},{}) intensity_cap={}", + mode_kind, period_ms, total_frames, colour1.r, colour1.g, colour1.b, intensity + ); + + let hid_for_task = hid.clone(); + let handle = runtime_handle.spawn(async move { + let mut ticker = + tokio::time::interval(Duration::from_millis(frame_ms)); + // Discard the immediate first tick so the loop pacing is stable. + ticker.tick().await; + let mut frame: u32 = 0; + loop { + let t = (frame % total_frames) as f32 / (total_frames as f32); + let (r, g, b, i) = match mode_kind { + AuraModeNum::Breathe => { + // Pure sinusoid on I; keep colour1 as the hue. + let s = (2.0 * std::f32::consts::PI * t).sin(); + let level = ((s + 1.0) * 0.5) * intensity as f32; + ( + colour1.r, + colour1.g, + colour1.b, + level.round().clamp(0.0, 255.0) as u8, + ) + } + AuraModeNum::Pulse => { + // Sharp attack, slow decay — "heartbeat" style. + let phase = t; + let level = if phase < 0.2 { + (phase / 0.2) * intensity as f32 + } else { + (1.0 - (phase - 0.2) / 0.8) * intensity as f32 + }; + ( + colour1.r, + colour1.g, + colour1.b, + level.round().clamp(0.0, 255.0) as u8, + ) + } + AuraModeNum::RainbowCycle => { + let hue = (t * 360.0) % 360.0; + let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); + (r, g, b, intensity) + } + AuraModeNum::RainbowWave => { + // On LampCount=1 there is no spatial "wave" to encode — + // a single lamp is scalar. Keep the same hue rotation as + // RainbowCycle but sweep hue backwards for a visual + // difference between the two modes. + let hue = (360.0 - (t * 360.0)) % 360.0; + let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); + (r, g, b, intensity) + } + // Should not happen: Static and unhandled modes go through + // the single-push path in write_effect. + _ => (colour1.r, colour1.g, colour1.b, intensity), + }; + let last = lamp_count - 1; + let payload = [ + 0x45, + 0x01, + 0x00, + 0x00, + (last & 0xff) as u8, + ((last >> 8) & 0xff) as u8, + r, + g, + b, + i, + ]; + // Hold the hid lock only for the write, so brightness/other + // callers can interleave between frames. + { + let hid = hid_for_task.lock().await; + if let Err(e) = hid.set_feature_report(&payload) { + log::warn!( + "lamparray_effect_task: set_feature_report failed: {e:?} — stopping" + ); + break; + } + } + frame = frame.wrapping_add(1); + ticker.tick().await; + } + debug_info!("lamparray_effect_task: exited"); + }); + Ok(handle) +} + +/// Map the abstract rog_aura::Speed enum to a period in milliseconds for +/// one full cycle of the animation. +pub fn speed_to_period_ms(s: Speed) -> u32 { + match s { + Speed::Low => 4000, + Speed::Med => 2000, + Speed::High => 800, + } +} + +/// Convert an HSV colour (hue in degrees, s/v in [0, 1]) to 8-bit RGB. +/// Standard formula from https://en.wikipedia.org/wiki/HSL_and_HSV. +pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { + let c = v * s; + let hp = (h / 60.0) % 6.0; + let x = c * (1.0 - ((hp % 2.0) - 1.0).abs()); + let (r1, g1, b1) = if hp < 1.0 { + (c, x, 0.0) + } else if hp < 2.0 { + (x, c, 0.0) + } else if hp < 3.0 { + (0.0, c, x) + } else if hp < 4.0 { + (0.0, x, c) + } else if hp < 5.0 { + (x, 0.0, c) + } else { + (c, 0.0, x) + }; + let m = v - c; + ( + ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, + ) +} diff --git a/asusd/src/aura_lamparray/mod.rs b/asusd/src/aura_lamparray/mod.rs new file mode 100644 index 00000000..c79d278a --- /dev/null +++ b/asusd/src/aura_lamparray/mod.rs @@ -0,0 +1,353 @@ +//! `aura_lamparray` — Microsoft HID LampArray (Usage Page 0x59) backend. +//! +//! This module is a sibling of `aura_slash`, `aura_anime`, `aura_scsi` and +//! deliberately kept separate from `aura_laptop`. The latter drives the ASUS +//! proprietary Aura HID protocol used by USB/asus-wmi keyboards; LampArray +//! is a standards-based feature-report protocol exposed by I2C-HID +//! controllers on newer ASUS TUF laptops (e.g. FA608WV / ITE5570). The two +//! share nothing at the wire level — collapsing them into one struct with +//! an `is_lamparray` flag turned out to be a source of coupling bugs +//! (config-lock deadlocks, double-push races on brightness change). +//! +//! Public surface: +//! * [`LampArray`] — owning struct, holds the `HidRaw` node, config, and +//! the currently-running dynamic-effect task handle. +//! * [`LampArrayZbus`] — zbus interface at `/xyz/ljones/aura/lamparray_`. +//! +//! The passive chip does not do animations on-die: the host must push +//! `LampRangeUpdate` (report 0x45) frames at ~30 FPS from a tokio task. +//! That task is created via [`LampArray::spawn_effect`] and cancelled by +//! [`LampArray::stop_effect_task`] on every new effect write, so two +//! loops can never race the hid lock. +use std::sync::Arc; + +use log::info; +use rog_aura::{AuraEffect, AuraModeNum, LedBrightness}; +use rog_platform::hid_raw::HidRaw; +use tokio::runtime::Handle; +use tokio::sync::{Mutex, MutexGuard}; +use tokio::task::JoinHandle; + +use crate::aura_laptop::config::AuraConfig; +use crate::error::RogError; + +pub mod effects; +pub mod trait_impls; + +use effects::spawn_effect_task; + +/// info! only in debug builds — release stays quiet. +/// +/// Wraps the low-level per-frame / per-step trace so `cargo build --release` +/// produces a journal that only contains lifecycle events (device +/// discovery, effect spawn/cancel, write_effect_and_apply). +macro_rules! debug_info { + ($($arg:tt)*) => {{ + if cfg!(debug_assertions) { + log::info!($($arg)*); + } + }}; +} + +#[derive(Debug, Clone)] +pub struct LampArray { + /// Underlying hidraw node. Feature reports are pushed through + /// `HidRaw::set_feature_report` / `get_feature_report`. + pub hid: Arc>, + /// Shared with the zbus interface. Owns brightness, current mode and the + /// stored builtin effects (colour1/colour2/speed per mode). + pub config: Arc>, + /// Handle for the currently-running LampArray dynamic-effect task. + /// Aborted on every effect write so we never accumulate loops. + pub effect_task: Arc>>>, + /// Tokio runtime handle captured at construction. The dynamic-effect + /// task must be spawned via `Handle::spawn` because methods on this + /// struct are invoked from the zbus executor thread, which is not a + /// Tokio runtime thread — bare `tokio::spawn()` would panic there. + pub runtime_handle: Handle, +} + +impl LampArray { + pub fn new(hid: Arc>, config: Arc>) -> Self { + debug_info!("LampArray constructed with runtime_handle captured"); + Self { + hid, + config, + effect_task: Arc::new(Mutex::new(None)), + runtime_handle: Handle::current(), + } + } + + pub async fn do_initialization(&self) -> Result<(), RogError> { + Ok(()) + } + + pub async fn lock_config(&self) -> MutexGuard<'_, AuraConfig> { + self.config.lock().await + } + + /// Write the currently active mode from config to the device. Mirrors + /// `Aura::write_current_config_mode` but for the LampArray path only. + /// Caller owns the config lock and passes it in — same reason as + /// `write_effect_locked`. + pub async fn write_current_config_mode( + &self, + config: &mut AuraConfig, + ) -> Result<(), RogError> { + if config.multizone_on { + let mode = config.current_mode; + let mut create = false; + if config.multizone.is_none() { + create = true; + } else if let Some(multizones) = config.multizone.as_ref() { + if !multizones.contains_key(&mode) { + create = true; + } + } + if create { + info!("No user-set config for zone founding, attempting a default"); + config.create_multizone_default()?; + } + if let Some(multizones) = config.multizone.as_mut() { + if let Some(set) = multizones.get(&mode) { + for mode in set.clone() { + self.write_effect_locked(config, &mode).await?; + } + } + } + } else { + let mode = config.current_mode; + if let Some(effect) = config.builtins.get(&mode).cloned() { + self.write_effect_locked(config, &effect).await?; + } + } + Ok(()) + } + + /// LampArray helper — write the current static colour to the whole + /// keyboard at the requested intensity (0-255). The protocol is the + /// Microsoft HID LampArray usage page: + /// * report 0x46 — "autonomous mode" toggle (we disable so the OS owns) + /// * report 0x41 — LampArrayAttributes (read to discover LampCount) + /// * report 0x45 — LampArrayMultiUpdate / RangeUpdate + pub async fn push_rgb_i( + &self, + r: u8, + g: u8, + b: u8, + intensity: u8, + ) -> Result<(), RogError> { + let hid = self.hid.lock().await; + // Disable autonomous so we own the lamp array + hid.set_feature_report(&[0x46, 0x00])?; + // Read LampArrayAttributes to discover the lamp count + let mut attr = vec![0u8; 23]; + attr[0] = 0x41; + hid.get_feature_report(&mut attr)?; + let lamp_count = u16::from_le_bytes([attr[1], attr[2]]); + if lamp_count == 0 { + return Err(RogError::MissingFunction( + "LampArray reports zero lamps".to_string(), + )); + } + let last = lamp_count - 1; + // RangeUpdate: 0x45, flags, start_lo, start_hi, end_lo, end_hi, r,g,b,i + let payload = [ + 0x45, + 0x01, + 0x00, + 0x00, + (last & 0xff) as u8, + ((last >> 8) & 0xff) as u8, + r, + g, + b, + intensity, + ]; + hid.set_feature_report(&payload)?; + info!( + "LampArray ready: LampCount={lamp_count} rgb=({r:02x},{g:02x},{b:02x}) i={intensity}" + ); + Ok(()) + } + + /// Write a single effect to a LampArray device. + /// + /// IMPORTANT: this used to take `self.config.lock().await` to read + /// brightness, but the typical call chain comes from a caller that ALREADY + /// holds the config lock (e.g. `write_current_config_mode`, + /// `set_led_mode_data`, `reload`). Re-locking caused an async deadlock at + /// init time, which made systemd kill asusd on the `Type=dbus` timeout. + /// We now use `try_lock` and fall back to `LedBrightness::Med` when the + /// lock is held by the caller. Callers that already have a locked + /// `AuraConfig` should prefer [`LampArray::write_effect_locked`] to avoid + /// the fallback path entirely. + pub async fn write_effect(&self, mode: &AuraEffect) -> Result<(), RogError> { + // Always stop any previous animation loop before doing anything else, + // so two effect tasks never race to push frames. + self.stop_effect_task().await; + let brightness = match self.config.try_lock() { + Ok(cfg) => cfg.brightness, + Err(_) => { + debug_info!( + "lamparray_write_effect: config already locked by caller, using Med fallback" + ); + LedBrightness::Med + } + }; + let intensity = Self::brightness_to_intensity(brightness); + match mode.mode { + AuraModeNum::Static => { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + debug_info!("lamparray_write_effect: Static, single push"); + debug_info!( + "lamparray_write_effect_locked: about to push rgb (caller owns config lock)" + ); + self.push_rgb_i(r, g, b, intensity).await + } + _ => { + debug_info!( + "lamparray_write_effect: dynamic mode {:?}, spawning effect task", + mode.mode + ); + self.spawn_effect(mode.clone(), intensity).await + } + } + } + + /// Variant for callers that already hold the config lock. Pass the + /// already-locked config in to avoid the deadlock that re-locking would + /// cause. + pub async fn write_effect_locked( + &self, + config: &AuraConfig, + mode: &AuraEffect, + ) -> Result<(), RogError> { + // Same rule as `write_effect`: kill any running animation before + // dispatching so we don't accumulate tasks across reloads. + self.stop_effect_task().await; + let intensity = Self::brightness_to_intensity(config.brightness); + match mode.mode { + AuraModeNum::Static => { + let r = mode.colour1.r; + let g = mode.colour1.g; + let b = mode.colour1.b; + debug_info!("lamparray_write_effect_locked: Static, single push"); + debug_info!( + "lamparray_write_effect_locked: about to push rgb (caller owns config lock)" + ); + self.push_rgb_i(r, g, b, intensity).await + } + _ => { + debug_info!( + "lamparray_write_effect_locked: dynamic mode {:?}, spawning effect task", + mode.mode + ); + self.spawn_effect(mode.clone(), intensity).await + } + } + } + + /// Brightness -> intensity mapping for LampArray. Reuses the colour from + /// the currently active builtin effect in config so the keyboard keeps + /// the same hue when the user only changes brightness. + /// + /// Uses `try_lock` to avoid the init-time deadlock when a caller higher + /// in the stack already owns the config lock (see comment on + /// [`LampArray::write_effect`]). + pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { + let level = match value { + 0 => LedBrightness::Off, + 1 => LedBrightness::Low, + 2 => LedBrightness::Med, + _ => LedBrightness::High, + }; + let intensity = Self::brightness_to_intensity(level); + let (r, g, b) = match self.config.try_lock() { + Ok(mut cfg) => { + cfg.brightness = level; + let mode = cfg.current_mode; + if let Some(eff) = cfg.builtins.get(&mode) { + (eff.colour1.r, eff.colour1.g, eff.colour1.b) + } else { + (0xff, 0xff, 0xff) + } + } + Err(_) => { + debug_info!( + "lamparray_set_brightness: config already locked by caller, defaulting to white" + ); + (0xff, 0xff, 0xff) + } + }; + debug_info!("lamparray_set_brightness: about to push rgb (no lock held)"); + self.push_rgb_i(r, g, b, intensity).await + } + + /// Aura power states on LampArray - we collapse the per-zone flags into a + /// simple on/off: any zone enabled -> full intensity with the saved RGB, + /// all disabled -> intensity 0. + pub async fn set_aura_power( + &self, + config: &AuraConfig, + ) -> Result<(), RogError> { + let any_on = config.enabled.states.iter().any(|s| { + // Treat the "new" zone state as on if any bit is set. + s.new_to_byte() != 0 + }); + let (r, g, b) = { + let mode = config.current_mode; + if let Some(eff) = config.builtins.get(&mode) { + (eff.colour1.r, eff.colour1.g, eff.colour1.b) + } else { + (0xff, 0xff, 0xff) + } + }; + let intensity = if any_on { 255 } else { 0 }; + // A power-state change also implies "stop whatever animation was + // running", otherwise the loop would happily override our push. + self.stop_effect_task().await; + self.push_rgb_i(r, g, b, intensity).await + } + + /// Abort the current LampArray effect task, if any. Safe to call even + /// when no task is running. + pub async fn stop_effect_task(&self) { + let mut slot = self.effect_task.lock().await; + if let Some(handle) = slot.take() { + handle.abort(); + info!("lamparray_effect_task: cancelled"); + } + } + + /// Spawn a tokio task that drives one of the dynamic LampArray effects + /// (Breathe / RainbowCycle / RainbowWave / Pulse). Delegates the frame + /// generation to `effects::spawn_effect_task`. + async fn spawn_effect( + &self, + mode: AuraEffect, + intensity: u8, + ) -> Result<(), RogError> { + let handle = spawn_effect_task( + self.hid.clone(), + &self.runtime_handle, + mode, + intensity, + ) + .await?; + let mut slot = self.effect_task.lock().await; + *slot = Some(handle); + Ok(()) + } + + fn brightness_to_intensity(b: LedBrightness) -> u8 { + match b { + LedBrightness::Off => 0, + LedBrightness::Low => 64, + LedBrightness::Med => 128, + LedBrightness::High => 255, + } + } +} diff --git a/asusd/src/aura_lamparray/trait_impls.rs b/asusd/src/aura_lamparray/trait_impls.rs new file mode 100644 index 00000000..f6ba7ce8 --- /dev/null +++ b/asusd/src/aura_lamparray/trait_impls.rs @@ -0,0 +1,281 @@ +//! zbus interface for LampArray devices. +//! +//! Registered at `/xyz/ljones/aura/lamparray_` — the path must stay +//! identical to the previous incarnation so `rog-control-center` keeps +//! working across the refactor. +//! +//! Exposes the same `xyz.ljones.Aura` interface name as [`AuraZbus`] so +//! clients cannot tell the two apart — the split is purely internal. +use std::collections::BTreeMap; + +use config_traits::StdConfig; +use log::{debug, error, info, warn}; +use rog_aura::keyboard::{AuraLaptopUsbPackets, LaptopAuraPower}; +use rog_aura::{AuraDeviceType, AuraEffect, AuraModeNum, AuraZone, LedBrightness, PowerZones}; +use zbus::fdo::Error as ZbErr; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::OwnedObjectPath; +use zbus::{interface, Connection}; + +use super::LampArray; +use crate::error::RogError; +use crate::{CtrlTask, Reloadable}; + +#[derive(Clone)] +pub struct LampArrayZbus(LampArray); + +impl LampArrayZbus { + pub fn new(lamparray: LampArray) -> Self { + Self(lamparray) + } + + pub async fn start_tasks( + mut self, + connection: &Connection, + path: OwnedObjectPath, + ) -> Result<(), RogError> { + self.reload() + .await + .unwrap_or_else(|err| warn!("Controller error: {}", err)); + connection + .object_server() + .at(path.clone(), self) + .await + .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) + .ok(); + Ok(()) + } +} + +/// The main interface for changing, reading, or notifying. +/// +/// Same interface name as the USB/asus-wmi [`AuraZbus`], so downstream +/// clients (`rog-control-center`, `asusctl aura`) interact with LampArray +/// devices without any protocol awareness. +#[interface(name = "xyz.ljones.Aura")] +impl LampArrayZbus { + /// Return the device type for this Aura keyboard + #[zbus(property)] + async fn device_type(&self) -> AuraDeviceType { + self.0.config.lock().await.led_type + } + + /// Return the current LED brightness (from config — LampArray has no + /// sysfs backlight node). + #[zbus(property)] + async fn brightness(&self) -> Result { + Ok(self.0.config.lock().await.brightness) + } + + /// Set the keyboard brightness level (0-3). + #[zbus(property)] + async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { + self.0.set_brightness(brightness.into()).await?; + let mut config = self.0.config.lock().await; + config.brightness = brightness; + config.write(); + Ok(()) + } + + /// Total levels of brightness available + #[zbus(property)] + async fn supported_brightness(&self) -> Vec { + vec![ + LedBrightness::Off, + LedBrightness::Low, + LedBrightness::Med, + LedBrightness::High, + ] + } + + /// The total available modes + #[zbus(property)] + async fn supported_basic_modes(&self) -> Result, ZbErr> { + let config = self.0.config.lock().await; + 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()) + } + + #[zbus(property)] + async fn supported_power_zones(&self) -> Result, ZbErr> { + let config = self.0.config.lock().await; + Ok(config.support_data.power_zones.clone()) + } + + /// The current mode data + #[zbus(property)] + async fn led_mode(&self) -> Result { + if let Ok(config) = self.0.config.try_lock() { + Ok(config.current_mode) + } else { + Err(ZbErr::Failed("LampArray control couldn't lock self".to_string())) + } + } + + /// Set an Aura effect if the effect mode or zone is supported. + /// + /// On success the aura config file is read to refresh cached values, + /// then the effect is stored and config written to disk. + #[zbus(property)] + async fn set_led_mode(&mut self, num: AuraModeNum) -> Result<(), ZbErr> { + let mut config = self.0.config.lock().await; + config.current_mode = num; + if config.brightness == LedBrightness::Off { + config.brightness = LedBrightness::Med; + } + self.0.write_current_config_mode(&mut config).await?; + // write_current_config_mode already pushed both colour and intensity + // in one HID feature report (via write_effect_locked). Avoid a + // second push that would race the colour with the white fallback in + // set_brightness. + config.write(); + Ok(()) + } + + /// The current mode data + #[zbus(property)] + async fn led_mode_data(&self) -> Result { + if let Ok(config) = self.0.config.try_lock() { + let mode = config.current_mode; + match config.builtins.get(&mode) { + Some(effect) => Ok(effect.clone()), + None => Err(ZbErr::Failed("Could not get the current effect".into())), + } + } else { + Err(ZbErr::Failed("LampArray control couldn't lock self".to_string())) + } + } + + /// Set an Aura effect if the effect mode or zone is supported. + /// + /// On success the aura config file is read to refresh cached values, + /// then the effect is stored and config written to disk. + #[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) + { + return Err(ZbErr::NotSupported(format!( + "The Aura effect is not supported: {effect:?}" + ))); + } + if config.brightness == LedBrightness::Off { + config.brightness = LedBrightness::Med; + } + // LampArray: a single HID feature report carries both colour and + // intensity, so we must push them together. Use write_effect_locked + // to avoid the try_lock fallback in write_effect that would clobber + // the colour with a Med/white default. Skip the subsequent + // set_brightness() call because set_brightness would race the + // colour we just wrote (try_lock fails -> white fallback push). + self.0.write_effect_locked(&config, &effect).await?; + config.set_builtin(effect); + config.write(); + Ok(()) + } + + /// Get the data set for every mode available + async fn all_mode_data(&self) -> BTreeMap { + let config = self.0.config.lock().await; + config.builtins.clone() + } + + #[zbus(property)] + async fn led_power(&self) -> LaptopAuraPower { + let config = self.0.config.lock().await; + config.enabled.clone() + } + + /// Set a variety of states, input is array of enum. + /// `enabled` sets if the sent array should be disabled or enabled. + #[zbus(property)] + async fn set_led_power(&mut self, options: LaptopAuraPower) -> Result<(), ZbErr> { + let mut config = self.0.config.lock().await; + for opt in options.states { + let zone = opt.zone; + for state in config.enabled.states.iter_mut() { + if state.zone == zone { + *state = opt; + break; + } + } + } + config.write(); + Ok(self.0.set_aura_power(&config).await.map_err(|e| { + warn!("{}", e); + e + })?) + } + + /// Direct addressing not supported on LampArray — the Microsoft HID + /// LampArray protocol has no per-key primitive on LampCount=1 devices. + /// Kept as a no-op so the interface signature matches [`AuraZbus`]. + async fn direct_addressing_raw(&self, _data: AuraLaptopUsbPackets) -> Result<(), ZbErr> { + debug!("LampArray: direct_addressing_raw ignored (no per-key primitive)"); + Ok(()) + } +} + +impl CtrlTask for LampArrayZbus { + fn zbus_path() -> &'static str { + "/xyz/ljones" + } + + async fn create_tasks(&self, _: SignalEmitter<'static>) -> Result<(), RogError> { + let inner_sleep = self.0.clone(); + let inner_shutdown = self.0.clone(); + self.create_sys_event_tasks( + move |sleeping| { + let inner = inner_sleep.clone(); + async move { + if !sleeping { + info!("LampArray CtrlKbdLedTask: reloading brightness and modes"); + let mut config = inner.config.lock().await; + inner + .write_current_config_mode(&mut config) + .await + .map_err(|e| { + error!("LampArray CtrlKbdLedTask: {e}"); + e + }) + .unwrap(); + } + } + }, + move |_shutting_down| { + let inner = inner_shutdown.clone(); + async move { + // Nothing to persist beyond config on shutdown for + // LampArray — brightness lives in config only. + let _ = inner; + } + }, + move |_lid_closed| async move {}, + move |_power_plugged| async move {}, + ) + .await; + Ok(()) + } +} + +impl Reloadable for LampArrayZbus { + async fn reload(&mut self) -> Result<(), RogError> { + debug!("reloading LampArray keyboard mode"); + let mut config = self.0.lock_config().await; + self.0.write_current_config_mode(&mut config).await?; + debug!("reloading LampArray power states"); + self.0 + .set_aura_power(&config) + .await + .map_err(|err| warn!("{err}")) + .ok(); + Ok(()) + } +} diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index 2254bd23..00aa802f 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -1,45 +1,31 @@ use std::sync::Arc; -use std::time::Duration; 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::{AuraDeviceType, AuraEffect, AuraModeNum, LedBrightness, PowerZones, Speed, AURA_LAPTOP_LED_MSG_LEN}; +use rog_aura::{AuraDeviceType, AuraEffect, PowerZones, AURA_LAPTOP_LED_MSG_LEN, LedBrightness}; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardBacklight; -use tokio::runtime::Handle; use tokio::sync::{Mutex, MutexGuard}; -use tokio::task::JoinHandle; use crate::error::RogError; pub mod config; pub mod trait_impls; +/// Aura keyboard controller for USB/asus-wmi path. +/// +/// This is the legacy path: HID over USB with the ASUS proprietary aura +/// protocol, or TUF sysfs backlight. LampArray devices (HID Usage Page 0x59 +/// on I2C-HID) live in the sibling module `aura_lamparray` and no longer +/// pollute this struct with `is_lamparray` branches. #[derive(Debug, Clone)] pub struct Aura { pub hid: Option>>, pub backlight: Option>>, pub config: Arc>, - /// True when this Aura is driven via the HID LampArray usage page - /// (I2C-HID controllers on newer ASUS TUF laptops). When true the - /// asus-wmi sysfs backlight is absent and all interactions go through - /// HIDIOC[SG]FEATURE feature reports. - pub is_lamparray: bool, - /// Handle for the currently-running LampArray dynamic-effect task. - /// The LampArray chip is passive: animations (Breathe, RainbowCycle, - /// RainbowWave, Pulse, ...) are driven by the host pushing - /// LampRangeUpdate frames at ~30 FPS from a tokio task. When a new - /// effect is written we abort the old task first so we never have two - /// loops fighting over the hid lock. - pub effect_task: Arc>>>, - /// Tokio runtime handle captured at construction. Used to spawn the - /// dynamic-effect task from methods invoked on the zbus executor thread - /// (which is *not* a Tokio runtime thread). `Handle::spawn()` works from - /// any thread; bare `tokio::spawn()` would panic there. - pub runtime_handle: Handle, } impl Aura { @@ -56,10 +42,7 @@ 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 self.is_lamparray { - // LampArray brightness lives entirely in our config (no sysfs node). - config.brightness.into() - } else if let Some(bl) = self.backlight.as_ref() { + let bright = if let Some(bl) = self.backlight.as_ref() { bl.lock().await.get_brightness().unwrap_or_default() } else { config.brightness.into() @@ -88,33 +71,20 @@ impl Aura { info!("No user-set config for zone founding, attempting a default"); config.create_multizone_default()?; } - if let Some(multizones) = config.multizone.as_mut() { if let Some(set) = multizones.get(&mode) { for mode in set.clone() { - if self.is_lamparray { - // Caller already owns `config`; re-locking - // `self.config` inside `lamparray_write_effect` - // used to deadlock us at init time. - self.lamparray_write_effect_locked(config, &mode).await?; - } else { - self.write_effect_and_apply(config.led_type, &mode).await?; - } + self.write_effect_and_apply(config.led_type, &mode).await?; } } } } else { let mode = config.current_mode; if let Some(effect) = config.builtins.get(&mode).cloned() { - if self.is_lamparray { - self.lamparray_write_effect_locked(config, &effect).await?; - } else { - self.write_effect_and_apply(config.led_type, &effect) - .await?; - } + self.write_effect_and_apply(config.led_type, &effect) + .await?; } } - Ok(()) } @@ -127,13 +97,6 @@ impl Aura { dev_type: AuraDeviceType, mode: &AuraEffect, ) -> Result<(), RogError> { - if self.is_lamparray { - info!( - "LampArray write_effect_and_apply: dev_type={:?} mode={:?}", - dev_type, mode.mode - ); - return self.lamparray_write_effect(mode).await; - } if matches!(dev_type, AuraDeviceType::LaptopKeyboardTuf) { if let Some(platform) = &self.backlight { let buf = [ @@ -164,14 +127,10 @@ impl Aura { } else { return Err(RogError::NoAuraKeyboard); } - Ok(()) } pub async fn set_brightness(&self, value: u8) -> Result<(), RogError> { - if self.is_lamparray { - return self.lamparray_set_brightness(value).await; - } if let Some(backlight) = &self.backlight { backlight.lock().await.set_brightness(value)?; return Ok(()); @@ -184,9 +143,6 @@ 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 self.is_lamparray { - return self.lamparray_set_aura_power(config).await; - } if matches!(config.led_type, rog_aura::AuraDeviceType::LaptopKeyboardTuf) { if let Some(backlight) = &self.backlight { // TODO: tuf bool array @@ -210,7 +166,6 @@ impl Aura { return Ok(()); } } - let bytes = config.enabled.to_bytes(config.led_type); let msg = [ 0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3], @@ -232,10 +187,8 @@ impl Aura { config.brightness = LedBrightness::Med; 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 { @@ -285,394 +238,4 @@ impl Aura { } Ok(()) } - - /// LampArray helper - write the current static colour to the whole - /// keyboard at the requested intensity (0-255). The protocol is the - /// Microsoft HID LampArray usage page: - /// * report 0x46 - "autonomous mode" toggle (we disable so the OS owns) - /// * report 0x41 - LampArrayAttributes (read to discover LampCount) - /// * report 0x45 - LampArrayMultiUpdate / RangeUpdate - async fn lamparray_push_rgb_i( - &self, - r: u8, - g: u8, - b: u8, - intensity: u8, - ) -> Result<(), RogError> { - let hid_arc = self - .hid - .as_ref() - .ok_or(RogError::NoAuraKeyboard)? - .clone(); - let hid = hid_arc.lock().await; - // Disable autonomous so we own the lamp array - hid.set_feature_report(&[0x46, 0x00])?; - // Read LampArrayAttributes to discover the lamp count - let mut attr = vec![0u8; 23]; - attr[0] = 0x41; - hid.get_feature_report(&mut attr)?; - let lamp_count = u16::from_le_bytes([attr[1], attr[2]]); - if lamp_count == 0 { - return Err(RogError::MissingFunction( - "LampArray reports zero lamps".to_string(), - )); - } - let last = lamp_count - 1; - // RangeUpdate: 0x45, flags, start_lo, start_hi, end_lo, end_hi, r,g,b,i - let payload = [ - 0x45, - 0x01, - 0x00, - 0x00, - (last & 0xff) as u8, - ((last >> 8) & 0xff) as u8, - r, - g, - b, - intensity, - ]; - hid.set_feature_report(&payload)?; - info!( - "LampArray ready: LampCount={lamp_count} rgb=({r:02x},{g:02x},{b:02x}) i={intensity}" - ); - Ok(()) - } - - /// Write a single effect (static colour for now) to a LampArray device. - /// - /// IMPORTANT: this used to take `self.config.lock().await` to read - /// brightness, but the typical call chain comes from a caller that ALREADY - /// holds the config lock (e.g. `write_current_config_mode`, - /// `set_led_mode_data`, `reload`). Re-locking caused an async deadlock at - /// init time, which made systemd kill asusd on the `Type=dbus` timeout. - /// We now use `try_lock` and fall back to `LedBrightness::Med` when the - /// lock is held by the caller. Callers that already have a locked - /// `AuraConfig` should prefer `lamparray_write_effect_locked` to avoid - /// the fallback path entirely. - pub async fn lamparray_write_effect(&self, mode: &AuraEffect) -> Result<(), RogError> { - // Always stop any previous animation loop before doing anything else, - // so two effect tasks never race to push frames. - self.lamparray_stop_effect_task().await; - let brightness = match self.config.try_lock() { - Ok(cfg) => cfg.brightness, - Err(_) => { - info!( - "lamparray_write_effect: config already locked by caller, using Med fallback" - ); - LedBrightness::Med - } - }; - let intensity = Self::brightness_to_intensity(brightness); - match mode.mode { - AuraModeNum::Static => { - let r = mode.colour1.r; - let g = mode.colour1.g; - let b = mode.colour1.b; - info!("lamparray_write_effect: Static, single push"); - self.lamparray_push_rgb_i(r, g, b, intensity).await - } - _ => { - info!( - "lamparray_write_effect: dynamic mode {:?}, spawning effect task", - mode.mode - ); - self.lamparray_spawn_effect(mode.clone(), intensity).await - } - } - } - - /// Variant for callers that already hold the config lock. Pass the - /// already-locked config in to avoid the deadlock that re-locking would - /// cause. - pub async fn lamparray_write_effect_locked( - &self, - config: &AuraConfig, - mode: &AuraEffect, - ) -> Result<(), RogError> { - // Same rule as `lamparray_write_effect`: kill any running animation - // before dispatching so we don't accumulate tasks across reloads. - self.lamparray_stop_effect_task().await; - let intensity = Self::brightness_to_intensity(config.brightness); - match mode.mode { - AuraModeNum::Static => { - let r = mode.colour1.r; - let g = mode.colour1.g; - let b = mode.colour1.b; - info!("lamparray_write_effect_locked: Static, single push"); - self.lamparray_push_rgb_i(r, g, b, intensity).await - } - _ => { - info!( - "lamparray_write_effect_locked: dynamic mode {:?}, spawning effect task", - mode.mode - ); - self.lamparray_spawn_effect(mode.clone(), intensity).await - } - } - } - - fn brightness_to_intensity(b: LedBrightness) -> u8 { - match b { - LedBrightness::Off => 0, - LedBrightness::Low => 64, - LedBrightness::Med => 128, - LedBrightness::High => 255, - } - } - - /// Brightness -> intensity mapping for LampArray. Reuses the colour from - /// the currently active builtin effect in config so the keyboard keeps - /// the same hue when the user only changes brightness. - /// - /// Uses `try_lock` to avoid the init-time deadlock when a caller higher - /// in the stack already owns the config lock (see comment on - /// `lamparray_write_effect`). - pub async fn lamparray_set_brightness(&self, value: u8) -> Result<(), RogError> { - let level = match value { - 0 => LedBrightness::Off, - 1 => LedBrightness::Low, - 2 => LedBrightness::Med, - _ => LedBrightness::High, - }; - let intensity = Self::brightness_to_intensity(level); - let (r, g, b) = match self.config.try_lock() { - Ok(mut cfg) => { - cfg.brightness = level; - let mode = cfg.current_mode; - if let Some(eff) = cfg.builtins.get(&mode) { - (eff.colour1.r, eff.colour1.g, eff.colour1.b) - } else { - (0xff, 0xff, 0xff) - } - } - Err(_) => { - info!( - "lamparray_set_brightness: config already locked by caller, defaulting to white" - ); - (0xff, 0xff, 0xff) - } - }; - info!("lamparray_set_brightness: about to push rgb (no lock held)"); - self.lamparray_push_rgb_i(r, g, b, intensity).await - } - - /// Aura power states on LampArray - we collapse the per-zone flags into a - /// simple on/off: any zone enabled -> full intensity with the saved RGB, - /// all disabled -> intensity 0. - pub async fn lamparray_set_aura_power( - &self, - config: &AuraConfig, - ) -> Result<(), RogError> { - let any_on = config.enabled.states.iter().any(|s| { - // Treat the "new" zone state as on if any bit is set. - s.new_to_byte() != 0 - }); - let (r, g, b) = { - let mode = config.current_mode; - if let Some(eff) = config.builtins.get(&mode) { - (eff.colour1.r, eff.colour1.g, eff.colour1.b) - } else { - (0xff, 0xff, 0xff) - } - }; - let intensity = if any_on { 255 } else { 0 }; - // A power-state change also implies "stop whatever animation was - // running", otherwise the loop would happily override our push. - self.lamparray_stop_effect_task().await; - self.lamparray_push_rgb_i(r, g, b, intensity).await - } - - /// Abort the current LampArray effect task, if any. Safe to call even - /// when no task is running. - pub async fn lamparray_stop_effect_task(&self) { - let mut slot = self.effect_task.lock().await; - if let Some(handle) = slot.take() { - handle.abort(); - info!("lamparray_effect_task: cancelled"); - } - } - - /// Spawn a tokio task that drives one of the dynamic LampArray effects - /// (Breathe / RainbowCycle / RainbowWave / Pulse). The task loops at - /// ~30 FPS pushing LampRangeUpdate feature reports. `intensity` is the - /// current brightness cap (0..=255): brightness-driven effects - /// (Breathe, Pulse) modulate I within this cap; HSV effects - /// (RainbowCycle, RainbowWave) pass it straight through. - async fn lamparray_spawn_effect( - &self, - mode: AuraEffect, - intensity: u8, - ) -> Result<(), RogError> { - let hid_arc = self - .hid - .as_ref() - .ok_or(RogError::NoAuraKeyboard)? - .clone(); - - // Probe LampCount once, up-front, so the task doesn't need to touch - // GET_FEATURE at 30 FPS. - let lamp_count = { - let hid = hid_arc.lock().await; - hid.set_feature_report(&[0x46, 0x00])?; - let mut attr = vec![0u8; 23]; - attr[0] = 0x41; - hid.get_feature_report(&mut attr)?; - u16::from_le_bytes([attr[1], attr[2]]) - }; - if lamp_count == 0 { - return Err(RogError::MissingFunction( - "LampArray reports zero lamps".to_string(), - )); - } - - let period_ms = Self::speed_to_period_ms(mode.speed); - let frame_ms: u64 = 33; // ~30 FPS - let total_frames: u32 = - ((period_ms as f32) / (frame_ms as f32)).max(1.0) as u32; - let mode_kind = mode.mode; - let colour1 = mode.colour1; - - info!( - "lamparray_effect_task: starting mode={:?} period={}ms frames={} rgb1=({},{},{}) intensity_cap={}", - mode_kind, - period_ms, - total_frames, - colour1.r, - colour1.g, - colour1.b, - intensity - ); - - let hid_for_task = hid_arc.clone(); - let handle = self.runtime_handle.spawn(async move { - let mut ticker = - tokio::time::interval(Duration::from_millis(frame_ms)); - // Discard the immediate first tick so the loop pacing is stable. - ticker.tick().await; - let mut frame: u32 = 0; - loop { - let t = (frame % total_frames) as f32 - / (total_frames as f32); - let (r, g, b, i) = match mode_kind { - AuraModeNum::Breathe => { - // Pure sinusoid on I; keep colour1 as the hue. - let s = (2.0 - * std::f32::consts::PI - * t) - .sin(); - let level = ((s + 1.0) * 0.5) * intensity as f32; - ( - colour1.r, - colour1.g, - colour1.b, - level.round().clamp(0.0, 255.0) as u8, - ) - } - AuraModeNum::Pulse => { - // Sharp attack, slow decay - "heartbeat" style. - let phase = t; - let level = if phase < 0.2 { - (phase / 0.2) * intensity as f32 - } else { - (1.0 - (phase - 0.2) / 0.8) * intensity as f32 - }; - ( - colour1.r, - colour1.g, - colour1.b, - level.round().clamp(0.0, 255.0) as u8, - ) - } - AuraModeNum::RainbowCycle => { - let hue = (t * 360.0) % 360.0; - let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); - (r, g, b, intensity) - } - AuraModeNum::RainbowWave => { - // On LampCount=1 there is no spatial "wave" to - // encode - a single lamp is scalar. We keep the - // same hue rotation as RainbowCycle but sweep the - // hue backwards to give the user a visual - // difference between the two modes. - let hue = (360.0 - (t * 360.0)) % 360.0; - let (r, g, b) = hsv_to_rgb(hue, 1.0, 1.0); - (r, g, b, intensity) - } - // Should not happen: Static and unhandled modes go - // through the single-push path in write_effect. - _ => (colour1.r, colour1.g, colour1.b, intensity), - }; - - let last = lamp_count - 1; - let payload = [ - 0x45, - 0x01, - 0x00, - 0x00, - (last & 0xff) as u8, - ((last >> 8) & 0xff) as u8, - r, - g, - b, - i, - ]; - // Hold the hid lock only for the write, so brightness/other - // callers can interleave between frames. - { - let hid = hid_for_task.lock().await; - if let Err(e) = hid.set_feature_report(&payload) { - log::warn!( - "lamparray_effect_task: set_feature_report failed: {e:?} - stopping" - ); - break; - } - } - - frame = frame.wrapping_add(1); - ticker.tick().await; - } - info!("lamparray_effect_task: exited"); - }); - - let mut slot = self.effect_task.lock().await; - *slot = Some(handle); - Ok(()) - } - - /// Map the abstract rog_aura::Speed enum to a period in milliseconds - /// for one full cycle of the animation. - fn speed_to_period_ms(s: Speed) -> u32 { - match s { - Speed::Low => 4000, - Speed::Med => 2000, - Speed::High => 800, - } - } -} - -/// Convert an HSV colour (hue in degrees, s/v in [0, 1]) to 8-bit RGB. -/// Standard formula from https://en.wikipedia.org/wiki/HSL_and_HSV. -fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { - let c = v * s; - let hp = (h / 60.0) % 6.0; - let x = c * (1.0 - ((hp % 2.0) - 1.0).abs()); - let (r1, g1, b1) = if hp < 1.0 { - (c, x, 0.0) - } else if hp < 2.0 { - (x, c, 0.0) - } else if hp < 3.0 { - (0.0, c, x) - } else if hp < 4.0 { - (0.0, x, c) - } else if hp < 5.0 { - (x, 0.0, c) - } else { - (c, 0.0, x) - }; - let m = v - c; - ( - ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, - ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, - ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8, - ) } diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index d5159cc7..7103e5f8 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -61,9 +61,6 @@ impl AuraZbus { /// Return the current LED brightness #[zbus(property)] async fn brightness(&self) -> Result { - if self.0.is_lamparray { - return Ok(self.0.config.lock().await.brightness); - } if let Some(bl) = self.0.backlight.as_ref() { return Ok(bl.lock().await.get_brightness().map(|n| n.into())?); } @@ -73,13 +70,6 @@ impl AuraZbus { /// Set the keyboard brightness level (0-3) #[zbus(property)] async fn set_brightness(&mut self, brightness: LedBrightness) -> Result<(), ZbErr> { - if self.0.is_lamparray { - self.0.lamparray_set_brightness(brightness.into()).await?; - let mut config = self.0.config.lock().await; - config.brightness = brightness; - config.write(); - return Ok(()); - } if let Some(bl) = self.0.backlight.as_ref() { let res = bl.lock().await.set_brightness(brightness.into()); if res.is_ok() { @@ -125,9 +115,6 @@ impl AuraZbus { /// The current mode data #[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 { @@ -147,13 +134,7 @@ impl AuraZbus { config.brightness = LedBrightness::Med; } self.0.write_current_config_mode(&mut config).await?; - if !self.0.is_lamparray { - // For LampArray write_current_config_mode already pushed both - // colour and intensity in one HID feature report (via the - // *_locked path). Avoid a second push that would race the - // colour with the white fallback in lamparray_set_brightness. - self.0.set_brightness(config.brightness.into()).await?; - } + self.0.set_brightness(config.brightness.into()).await?; config.write(); Ok(()) } @@ -161,7 +142,6 @@ impl AuraZbus { /// The current mode data #[zbus(property)] async fn led_mode_data(&self) -> Result { - // entirely possible to deadlock here, so use try instead of lock() if let Ok(config) = self.0.config.try_lock() { let mode = config.current_mode; match config.builtins.get(&mode) { @@ -188,28 +168,13 @@ impl AuraZbus { "The Aura effect is not supported: {effect:?}" ))); } - if config.brightness == LedBrightness::Off { config.brightness = LedBrightness::Med; } - if self.0.is_lamparray { - // LampArray: a single HID feature report carries both colour and - // intensity, so we must push them together. The caller already - // owns config.lock(), so use the *_locked variant to avoid the - // try_lock fallback in lamparray_write_effect that would clobber - // the colour with a Med/white default. We also skip the - // subsequent set_brightness() call because lamparray_set_brightness - // would race the colour we just wrote (try_lock fails -> white - // fallback push). - self.0 - .lamparray_write_effect_locked(&config, &effect) - .await?; - } else { - self.0 - .write_effect_and_apply(config.led_type, &effect) - .await?; - self.0.set_brightness(config.brightness.into()).await?; - } + self.0 + .write_effect_and_apply(config.led_type, &effect) + .await?; + self.0.set_brightness(config.brightness.into()).await?; config.set_builtin(effect); config.write(); Ok(()) @@ -336,28 +301,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 da918fe6..d523bfd9 100644 --- a/asusd/src/aura_manager.rs +++ b/asusd/src/aura_manager.rs @@ -3,7 +3,6 @@ // - If a device is found, add it to watch // - Add it to Zbus server // - If udev sees device removed then remove the zbus path - use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -18,6 +17,7 @@ use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use zbus::Connection; use crate::aura_anime::trait_impls::AniMeZbus; +use crate::aura_lamparray::trait_impls::LampArrayZbus; use crate::aura_laptop::trait_impls::AuraZbus; use crate::aura_scsi::trait_impls::ScsiZbus; use crate::aura_slash::trait_impls::SlashZbus; @@ -27,6 +27,15 @@ use crate::ASUS_ZBUS_PATH; const MOD_NAME: &str = "aura"; +/// info! only in debug builds — release stays quiet. +macro_rules! debug_info { + ($($arg:tt)*) => {{ + if cfg!(debug_assertions) { + log::info!($($arg)*); + } + }}; +} + /// Returns only the Device details concatenated in a form usable for /// adding/appending to a filename pub fn filename_partial(parent: &Device) -> Option { @@ -110,11 +119,9 @@ impl DeviceManager { .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()); @@ -132,9 +139,10 @@ impl DeviceManager { .devnode() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "".to_string()); - info!( + debug_info!( "init_hid_devices: probing hidraw sysname={sysname_dbg} devnode={devnode_dbg}" ); + if let Some(usb_device) = device.parent_with_subsystem_devtype("usb", "usb_device")? { if let Some(usb_id) = usb_device.attribute_value("idProduct") { if let Some(vendor_id) = usb_device.attribute_value("idVendor") { @@ -215,12 +223,12 @@ impl DeviceManager { } } if devices.is_empty() { - info!( + debug_info!( "init_hid_devices: no USB-side device matched for {sysname_dbg}, trying I2C-HID fallback" ); match Self::init_i2c_hid_device(connection, &device, handles.clone()).await { Ok(mut found) => { - info!( + debug_info!( "init_hid_devices: I2C-HID fallback for {sysname_dbg} returned {} device(s)", found.len() ); @@ -243,7 +251,7 @@ impl DeviceManager { ) -> Result, RogError> { let mut devices = Vec::new(); let sysname = endpoint.sysname().to_string_lossy().to_string(); - info!("I2C-HID probe: examining hidraw {sysname}"); + debug_info!("I2C-HID probe: examining hidraw {sysname}"); let mut hid_id: Option = None; let mut cur = endpoint.parent(); while let Some(p) = cur { @@ -254,13 +262,13 @@ impl DeviceManager { cur = p.parent(); } let Some(hid_id) = hid_id else { - info!("I2C-HID probe: no HID_ID property found for {sysname}"); + debug_info!("I2C-HID probe: no HID_ID property found for {sysname}"); return Ok(devices); }; - info!("I2C-HID probe: {sysname} HID_ID={hid_id}"); + debug_info!("I2C-HID probe: {sysname} HID_ID={hid_id}"); let parts: Vec<&str> = hid_id.split(':').collect(); if parts.len() != 3 { - info!("I2C-HID probe: HID_ID has unexpected shape: {hid_id}"); + debug_info!("I2C-HID probe: HID_ID has unexpected shape: {hid_id}"); return Ok(devices); } // The HID_ID format is "BUS:VENDOR:PRODUCT" where VENDOR and PRODUCT are @@ -271,11 +279,11 @@ impl DeviceManager { let product_str = parts[2]; let vendor = u32::from_str_radix(vendor_str, 16).unwrap_or(0xFFFF_FFFF); let product = u32::from_str_radix(product_str, 16).unwrap_or(0xFFFF_FFFF); - info!( + debug_info!( "I2C-HID probe: VID parsing raw='{vendor_str}' parsed=0x{vendor:08x}; PID parsing raw='{product_str}' parsed=0x{product:08x}" ); if vendor != 0x0b05 { - info!( + debug_info!( "I2C-HID probe: not ASUS vendor 0x{vendor:08x} (raw '{vendor_str}'), skipping {sysname}" ); return Ok(devices); @@ -286,8 +294,8 @@ impl DeviceManager { info!( "Found ASUS HID LampArray candidate: {sysname} VID={vid_upper} PID={pid_upper}" ); - info!("VID match: proceeding to open hidraw for {sysname}"); - info!("Step1: about to query devnode for {sysname}"); + debug_info!("VID match: proceeding to open hidraw for {sysname}"); + debug_info!("Step1: about to query devnode for {sysname}"); let dev_node = match endpoint.devnode() { Some(n) => n, None => { @@ -297,24 +305,27 @@ impl DeviceManager { )); } }; - info!("Step2: devnode={dev_node:?} for {sysname}"); - info!("I2C-HID probe: devnode={dev_node:?} for {sysname}"); + debug_info!("Step2: devnode={dev_node:?} for {sysname}"); let key = dev_node.to_string_lossy().to_string(); - info!("Step3a: about to take handles map lock for cache lookup key={key}"); + debug_info!("Step3a: about to take handles map lock for cache lookup key={key}"); let cached = handles.lock().await.get(&key).cloned(); - info!("Step3b: handles map lock released for {key} cached={}", cached.is_some()); + debug_info!( + "Step3b: handles map lock released for {key} cached={}", + cached.is_some() + ); let handle = if let Some(existing) = cached { - info!("I2C-HID probe: reusing existing hidraw handle for {key}"); + debug_info!("I2C-HID probe: reusing existing hidraw handle for {key}"); existing } else { - info!("Step3c: about to call HidRaw::from_i2c_device for {key} prod_id={prod_id_str}"); - info!("I2C-HID probe: creating HidRaw::from_i2c_device for {key} prod_id={prod_id_str}"); + debug_info!( + "Step3c: about to call HidRaw::from_i2c_device for {key} prod_id={prod_id_str}" + ); // udev::Device contains a raw `*mut udev` and is !Send, so we // cannot ship it into spawn_blocking. Instead we call the - // synchronous constructor here -- the actual blocking syscall is + // synchronous constructor here — the actual blocking syscall is // the OpenOptions::open in from_i2c_device, and we mitigate that // separately by passing O_NONBLOCK there. - info!("Step3d: calling HidRaw::from_i2c_device synchronously for {key}"); + debug_info!("Step3d: calling HidRaw::from_i2c_device synchronously for {key}"); let hidraw_res = HidRaw::from_i2c_device(endpoint.clone(), &prod_id_str); let hidraw = match hidraw_res { Ok(h) => h, @@ -325,30 +336,31 @@ impl DeviceManager { return Err(e.into()); } }; - info!("Step4: hidraw handle created for {key}"); - info!("I2C-HID probe: HidRaw::from_i2c_device returned OK for {key}"); + debug_info!("Step4: hidraw handle created for {key}"); let h = Arc::new(Mutex::new(hidraw)); - info!("Step4b: about to insert handle into handles map key={key}"); + debug_info!("Step4b: about to insert handle into handles map key={key}"); handles.lock().await.insert(key.clone(), h.clone()); - info!("Step4c: handle inserted into handles map key={key}"); + debug_info!("Step4c: handle inserted into handles map key={key}"); h }; - info!("Step5: about to call DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}"); - info!("Calling DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}"); + debug_info!( + "Step5: about to call DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}" + ); + debug_info!("Calling DeviceHandle::maybe_lamparray for {sysname} prod_id={prod_id_str}"); let result = DeviceHandle::maybe_lamparray(handle, &prod_id_str).await; - info!("Step6: maybe_lamparray returned for {sysname}"); + debug_info!("Step6: maybe_lamparray returned for {sysname}"); match result { Ok(dev_type) => { - info!("maybe_lamparray OK for {sysname}"); - if let DeviceHandle::Aura(aura) = dev_type.clone() { + debug_info!("maybe_lamparray OK for {sysname}"); + if let DeviceHandle::LampArray(lamparray) = dev_type.clone() { let path: OwnedObjectPath = ObjectPath::from_str_unchecked(&format!( "{ASUS_ZBUS_PATH}/{MOD_NAME}/lamparray_{prod_id_str}" )) .into(); info!("Registering LampArray device on zbus at path={path:?}"); - let ctrl = AuraZbus::new(aura); + let ctrl = LampArrayZbus::new(lamparray); match ctrl.start_tasks(connection, path.clone()).await { - Ok(_) => info!("LampArray zbus start_tasks OK for {path:?}"), + Ok(_) => debug_info!("LampArray zbus start_tasks OK for {path:?}"), Err(e) => { error!( "LampArray zbus start_tasks FAILED for {path:?}: {e:?}" @@ -361,10 +373,10 @@ impl DeviceManager { dbus_path: path.clone(), hid_key: Some(key), }); - info!("LampArray device added to manager at {path:?}"); + debug_info!("LampArray device added to manager at {path:?}"); } else { - info!( - "maybe_lamparray returned non-Aura variant for {sysname}, ignoring" + debug_info!( + "maybe_lamparray returned non-LampArray variant for {sysname}, ignoring" ); } } @@ -386,17 +398,15 @@ impl DeviceManager { // 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(); - let mut enumerator = udev::Enumerator::new().map_err(|err| { warn!("{}", err); PlatformError::Udev("enumerator failed".into(), err) })?; - enumerator.match_subsystem("hidraw").map_err(|err| { warn!("{}", err); PlatformError::Udev("match_subsystem failed".into(), err) })?; - + debug_info!("LampArray initial enumeration: scanning hidraw devices"); for device in enumerator .scan_devices() .map_err(|e| PlatformError::IoPath("enumerator".to_owned(), e))? @@ -414,7 +424,6 @@ impl DeviceManager { } devices.append(&mut Self::init_hid_devices(connection, device, handles.clone()).await?); } - Ok(devices) } @@ -453,17 +462,14 @@ impl DeviceManager { // track and ensure we use only one hidraw per prod_id // let mut interfaces = HashSet::new(); let mut devices: Vec = Vec::new(); - let mut enumerator = udev::Enumerator::new().map_err(|err| { warn!("{}", err); PlatformError::Udev("enumerator failed".into(), err) })?; - enumerator.match_subsystem("block").map_err(|err| { warn!("{}", err); PlatformError::Udev("match_subsystem failed".into(), err) })?; - let mut found = Vec::new(); for device in enumerator .scan_devices() @@ -475,7 +481,6 @@ impl DeviceManager { if found.contains(&path) { continue; } - if let Some(dev) = Self::init_scsi(connection, &device, path.clone()).await { devices.push(dev); found.push(path); @@ -484,7 +489,6 @@ impl DeviceManager { debug!("No serial for SCSI device: {:?}", device.devpath()); } } - Ok(devices) } @@ -508,11 +512,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(_) | DeviceHandle::OldAura(_) | DeviceHandle::LampArray(_) + ) { do_kb_backlight = false; } } - if do_slash { if let Ok(dev_type) = DeviceHandle::new_slash_usb().await { if let DeviceHandle::Slash(slash) = dev_type.clone() { @@ -529,7 +535,6 @@ impl DeviceManager { info!("Tested device was not Slash"); } } - if do_anime { if let Ok(dev_type) = DeviceHandle::maybe_anime_usb().await { // TODO: this is copy/pasted @@ -553,7 +558,6 @@ impl DeviceManager { info!("Tested device was not AniMe Matrix"); } } - if do_kb_backlight { // TUF AURA LAPTOP DEVICE // product_name = ASUS TUF Gaming F15 FX507ZE_FX507ZE @@ -579,11 +583,9 @@ impl DeviceManager { } } } - if let Ok(devs) = &mut Self::init_all_scsi(connection).await { devices.append(devs); } - devices } @@ -597,10 +599,8 @@ impl DeviceManager { _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()?; @@ -608,7 +608,6 @@ impl DeviceManager { let mut events = Events::with_capacity(1024); poll.registry() .register(&mut monitor, Token(0), Interest::READABLE)?; - let rt = tokio::runtime::Runtime::new().expect("Unable to create Runtime"); let _enter = rt.enter(); loop { @@ -621,13 +620,11 @@ impl DeviceManager { .unwrap_or_default() .to_string_lossy() .to_string(); - let subsys = if let Some(subsys) = event.subsystem() { subsys.to_string_lossy().to_string() } else { continue; }; - let devices = devices.clone(); let conn_copy = conn_copy.clone(); let hid_handles = hid_handles_thread.clone(); @@ -640,7 +637,6 @@ impl DeviceManager { { let serial = serial.to_string_lossy().to_string(); let path = dbus_path_for_scsi(&serial); - let index = if let Some(index) = devices .lock() .await @@ -678,7 +674,6 @@ impl DeviceManager { } }; } - if subsys == "hidraw" { if action == "remove" { // Key cleanup off the removed hidraw node itself, NOT the @@ -725,6 +720,12 @@ impl DeviceManager { .remove::(&path) .await? } + DeviceHandle::LampArray(_) => { + conn_copy + .object_server() + .remove::(&path) + .await? + } DeviceHandle::Slash(_) => { conn_copy .object_server() diff --git a/asusd/src/aura_types.rs b/asusd/src/aura_types.rs index 9951b85b..b304c20d 100644 --- a/asusd/src/aura_types.rs +++ b/asusd/src/aura_types.rs @@ -16,6 +16,7 @@ use tokio::sync::Mutex; use crate::aura_anime::config::AniMeConfig; use crate::aura_anime::AniMe; +use crate::aura_lamparray::LampArray; use crate::aura_laptop::config::AuraConfig; use crate::aura_laptop::Aura; use crate::aura_scsi::config::ScsiConfig; @@ -24,6 +25,15 @@ use crate::aura_slash::config::SlashConfig; use crate::aura_slash::Slash; use crate::error::RogError; +/// info! only in debug builds — release stays quiet. +macro_rules! debug_info { + ($($arg:tt)*) => {{ + if cfg!(debug_assertions) { + log::info!($($arg)*); + } + }}; +} + pub enum _DeviceHandle { /// The AniMe devices require USBRaw as they are not HID devices Usb(USBRaw), @@ -37,6 +47,7 @@ pub enum _DeviceHandle { #[derive(Clone)] pub enum DeviceHandle { Aura(Aura), + LampArray(LampArray), Slash(Slash), /// The AniMe devices require USBRaw as they are not HID devices AniMe(AniMe), @@ -69,7 +80,6 @@ impl DeviceHandle { return Err(RogError::NotFound("No slash device".to_string())); } info!("Found slash type {slash_type:?}: {prod_id}"); - let mut config = SlashConfig::new().load(); config.slash_type = slash_type; let slash = Slash::new(Some(device), None, Arc::new(Mutex::new(config))); @@ -84,10 +94,8 @@ impl DeviceHandle { 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( @@ -107,25 +115,9 @@ impl DeviceHandle { _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 { @@ -135,10 +127,8 @@ impl DeviceHandle { info!("No Anime Matrix capable laptop found"); return Err(RogError::Anime(AnimeError::NoDevice)); } - if let Ok(usb) = USBRaw::new(0x193b) { info!("Found AniMe Matrix USB {anime_type:?}"); - let mut config = AniMeConfig::new().load(); config.anime_type = anime_type; let mut anime = AniMe::new( @@ -163,7 +153,6 @@ impl DeviceHandle { return Err(RogError::NotFound("No SCSI device".to_string())); } info!("Found SCSI device {prod_id:?} on {dev_node}"); - let mut config = ScsiConfig::new().load(); config.dev_type = AuraDeviceType::ScsiExtDisk; let dev = Arc::new(Mutex::new(open_device(dev_node)?)); @@ -189,14 +178,12 @@ impl DeviceHandle { return Err(RogError::NotFound("No laptop aura device".to_string())); } info!("Found laptop aura type {prod_id:?}"); - let backlight = KeyboardBacklight::new() .map_err(|e| error!("Keyboard backlight error: {e:?}")) .map_or(None, |k| { info!("Found sysfs backlight control"); Some(Arc::new(Mutex::new(k))) }); - // 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; @@ -204,9 +191,6 @@ impl DeviceHandle { hid: device, backlight, config: Arc::new(Mutex::new(config)), - is_lamparray: false, - effect_task: Arc::new(Mutex::new(None)), - runtime_handle: tokio::runtime::Handle::current(), }; aura.do_initialization().await?; Ok(Self::Aura(aura)) @@ -224,8 +208,8 @@ impl DeviceHandle { let g = device.lock().await; g.devfs_path().clone() }; - info!("LampArray init: device {dev_path:?} prod_id={prod_id:?}"); - info!("LampArray init: about to call raw_info on {dev_path:?}"); + debug_info!("LampArray init: device {dev_path:?} prod_id={prod_id:?}"); + debug_info!("LampArray init: about to call raw_info on {dev_path:?}"); let info_res = { let g = device.lock().await; g.raw_info() @@ -239,7 +223,7 @@ impl DeviceHandle { }; let vid = (info.vendor as u16) as u32; let pid = (info.product as u16) as u32; - info!( + debug_info!( "LampArray init: HIDIOCGRAWINFO {dev_path:?} VID:{vid:04x} PID:{pid:04x}" ); if vid != 0x0b05 { @@ -254,16 +238,9 @@ impl DeviceHandle { info!("Found HID LampArray ASUS keyboard 0b05:{pid:04x}"); let mut config = AuraConfig::load_and_update_config(prod_id); config.led_type = aura_type; - let aura = Aura { - hid: Some(device), - backlight: None, - config: Arc::new(Mutex::new(config)), - is_lamparray: true, - effect_task: Arc::new(Mutex::new(None)), - runtime_handle: tokio::runtime::Handle::current(), - }; - aura.do_initialization().await?; + let lamparray = LampArray::new(device, Arc::new(Mutex::new(config))); + lamparray.do_initialization().await?; info!("LampArray ready: 0b05:{pid:04x} on {dev_path:?}"); - Ok(Self::Aura(aura)) + Ok(Self::LampArray(lamparray)) } } diff --git a/asusd/src/lib.rs b/asusd/src/lib.rs index 19d0549a..da4840e1 100644 --- a/asusd/src/lib.rs +++ b/asusd/src/lib.rs @@ -12,6 +12,7 @@ pub mod ctrl_xgm_led; pub mod asus_armoury; pub mod aura_anime; +pub mod aura_lamparray; pub mod aura_laptop; pub mod aura_manager; pub mod aura_scsi; From 8d825a8d1c9066cc0adb670f0e1708073a1b5e41 Mon Sep 17 00:00:00 2001 From: vezzo Date: Wed, 1 Jul 2026 16:17:27 +0200 Subject: [PATCH 9/9] aura: gate granular debug logs behind cfg(debug_assertions) Wrap the Step1..Step6 discovery, HidRaw::from_i2c_device trace, raw_info ioctl trace, and other low-level lifecycle logs in a debug_info! macro that expands to info! only when built with debug assertions. Release builds stay quiet; developers debugging PID whitelist or i2c-hid open blocking issues still get the full trace. The debug_info! macro was introduced in the aura_lamparray refactor for the LampArray-specific probe/effect trace; this commit extends the same pattern to the underlying rog-platform HidRaw ioctl helpers so the release journal is fully quiet. Retains top-level info!: - LampArray device discovery outcomes (Found candidate, ready) - Effect task lifecycle (spawn / cancel / write_effect_and_apply) - Startup lifecycle (Startup ready, DeviceManager initialized) --- rog-platform/src/hid_raw.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/rog-platform/src/hid_raw.rs b/rog-platform/src/hid_raw.rs index bb8e774e..d2d07d4d 100644 --- a/rog-platform/src/hid_raw.rs +++ b/rog-platform/src/hid_raw.rs @@ -9,6 +9,21 @@ use log::{error, info, warn}; use udev::Device; use crate::error::{PlatformError, Result}; +/// info! only in debug builds — release stays quiet. +/// +/// Wraps the low-level per-step / per-syscall trace so `cargo build --release` +/// produces a journal that only contains lifecycle events. The identical +/// definition lives in `asusd/src/aura_lamparray/mod.rs` and friends; kept +/// duplicated because we don't want a shared debug-log crate for one macro. +macro_rules! debug_info { + ($($arg:tt)*) => {{ + if cfg!(debug_assertions) { + log::info!($($arg)*); + } + }}; +} + + /// Matches the kernel `struct hidraw_devinfo` (8 bytes total). #[repr(C)] pub struct HidrawDevinfo { @@ -143,19 +158,19 @@ impl HidRaw { .sysname() .to_string_lossy() .to_string(); - info!( + debug_info!( "HidRaw::from_i2c_device: begin sysname={} prod_id={}", sysname_dbg, prod_id ); - info!("HidRaw::from_i2c_device: querying devnode for sysname={}", sysname_dbg); + debug_info!("HidRaw::from_i2c_device: querying devnode for sysname={}", sysname_dbg); let dev_node = endpoint.devnode().ok_or_else(|| { PlatformError::MissingFunction("I2C-HID endpoint has no devnode".to_string()) })?; - info!( + debug_info!( "HidRaw::from_i2c_device: devnode={:?} sysname={}", dev_node, sysname_dbg ); - info!( + debug_info!( "HidRaw::from_i2c_device: opening {:?} R/W (O_NONBLOCK) for prod_id={}", dev_node, prod_id ); @@ -166,17 +181,17 @@ impl HidRaw { .open(dev_node) .map_err(|e| PlatformError::IoPath(dev_node.to_string_lossy().to_string(), e))?; let fd = file.as_raw_fd(); - info!( + debug_info!( "HidRaw::from_i2c_device: file opened fd={} dev_node={:?}", fd, dev_node ); - info!("HidRaw::from_i2c_device: about to query syspath for sysname={}", sysname_dbg); + debug_info!("HidRaw::from_i2c_device: about to query syspath for sysname={}", sysname_dbg); let syspath = endpoint.syspath().to_path_buf(); - info!( + debug_info!( "HidRaw::from_i2c_device: syspath={:?} sysname={}", syspath, sysname_dbg ); - info!( + debug_info!( "HidRaw::from_i2c_device: returning OK for sysname={} fd={}", sysname_dbg, fd ); @@ -226,7 +241,7 @@ impl HidRaw { vendor: 0, product: 0, }; - info!( + debug_info!( "HidRaw::raw_info: fd={} ioctl=0x{:08x} struct_size={}", fd, req, @@ -252,7 +267,7 @@ impl HidRaw { err, )); } - info!( + debug_info!( "HidRaw::raw_info: ok bus={:#x} vendor={:#06x} product={:#06x}", info.bustype, info.vendor as u16,