From 0871e67d5d4002f7f34e1920b1094fcba4d07eda Mon Sep 17 00:00:00 2001 From: ido <106871003+idobutidont@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:43:55 +0700 Subject: [PATCH 1/2] fix(rog-aura): support TUF FA401 series keyboard power states Add FA401 series support for the TUF keyboard RGB power-state controls and sleep-backlight handling. This requires the corresponding asus-wmi kernel change to fully work: Link: https://lore.kernel.org/platform-driver-x86/20260902032353.16106-1-idotohors@gmail.com --- asusctl/src/main.rs | 7 ++- asusd/src/aura_laptop/mod.rs | 21 ++++++- asusd/src/aura_laptop/trait_impls.rs | 82 ++++++++++++++++++---------- asusd/src/lib.rs | 19 +++++-- rog-aura/data/aura_support.ron | 9 +++ rog-aura/src/keyboard/power.rs | 15 +++++ 6 files changed, 114 insertions(+), 39 deletions(-) diff --git a/asusctl/src/main.rs b/asusctl/src/main.rs index 470180ccf..96811a50d 100644 --- a/asusctl/src/main.rs +++ b/asusctl/src/main.rs @@ -668,7 +668,10 @@ fn handle_led_power_1_do_1866( power: &LedPowerCommand1, ) -> Result<(), Box> { let mut states = Vec::new(); - if power.keyboard { + // TUF exposes one keyboard zone; the generic 0x1866 command may expose + // separate keyboard and lightbar zones. + let is_tuf = aura.device_type()?.is_tuf_laptop(); + if power.keyboard || (is_tuf && power.lightbar) { states.push(AuraPowerState { zone: PowerZones::Keyboard, boot: power.boot.unwrap_or_default(), @@ -677,7 +680,7 @@ fn handle_led_power_1_do_1866( shutdown: false, }); } - if power.lightbar { + if power.lightbar && !is_tuf { states.push(AuraPowerState { zone: PowerZones::Lightbar, boot: power.boot.unwrap_or_default(), diff --git a/asusd/src/aura_laptop/mod.rs b/asusd/src/aura_laptop/mod.rs index f6ebef2ca..3990f58bb 100644 --- a/asusd/src/aura_laptop/mod.rs +++ b/asusd/src/aura_laptop/mod.rs @@ -41,7 +41,16 @@ impl Aura { } else { config.brightness.into() }; + // These fields are derived during device discovery and deliberately + // excluded from the on-disk configuration. `read()` replaces the + // entire struct, so retain them when refreshing settings for sleep. + let led_type = config.led_type; + let support_data = config.support_data.clone(); + let per_key_mode_active = config.per_key_mode_active; config.read(); + config.led_type = led_type; + config.support_data = support_data; + config.per_key_mode_active = per_key_mode_active; config.brightness = bright.into(); config.write(); Ok(()) @@ -144,7 +153,17 @@ impl Aura { if let Some(backlight) = &self.backlight { // TODO: tuf bool array let buf = config.enabled.to_bytes(config.led_type); - backlight.lock().await.set_kbd_rgb_state(&buf)?; + let backlight = backlight.lock().await; + // Some FA401UH firmware/kernel combinations expose brightness + // and RGB mode but not the optional power-state attribute. + // Power changes must not make the whole Aura interface fail. + if backlight.has_kbd_rgb_state() { + backlight.set_kbd_rgb_state(&buf)?; + } else { + log::debug!( + "TUF keyboard does not expose kbd_rgb_state; skipping power-state write" + ); + } } } else if let Some(hid_raw) = &self.hid { let hid_raw = hid_raw.lock().await; diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index 5cd692b20..fa9cc7817 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -27,7 +27,6 @@ impl AuraZbus { pub async fn start_tasks( mut self, connection: &Connection, - // _signal_ctx: SignalEmitter<'static>, path: OwnedObjectPath, ) -> Result<(), RogError> { // let task = zbus.clone(); @@ -35,14 +34,20 @@ impl AuraZbus { self.reload() .await .unwrap_or_else(|err| warn!("Controller error: {}", err)); + let task = self.clone(); connection .object_server() .at(path.clone(), self) .await .map_err(|e| error!("Couldn't add server at path: {path}, {e:?}")) .ok(); - // TODO: skip this until we keep handles to tasks so they can be killed - // task.create_tasks(signal_ctx).await + // Subscribe to logind sleep/shutdown events. Without this call the + // Aura interface is available, but the keyboard controller never + // receives on_prepare_for_sleep notifications. + let signal_ctx = SignalEmitter::new(connection, AURA_ZBUS_PATH)?; + info!("Starting CtrlKbdLedTask system-event subscription"); + task.create_tasks(signal_ctx).await?; + info!("Started CtrlKbdLedTask system-event subscription"); Ok(()) } } @@ -241,6 +246,7 @@ impl CtrlTask for AuraZbus { } async fn create_tasks(&self, _: SignalEmitter<'static>) -> Result<(), RogError> { + info!("Creating Aura system-event callbacks"); let inner1 = self.0.clone(); let inner3 = self.0.clone(); self.create_sys_event_tasks( @@ -248,37 +254,53 @@ impl CtrlTask for AuraZbus { let inner1 = inner1.clone(); // unwrap as we want to bomb out of the task async move { - if !sleeping { + info!("CtrlKbdLedTask received prepare_for_sleep({sleeping})"); + if sleeping { + // Re-write the user's configured power state right + // before suspend. The kernel patch re-asserts brightness + // and enables all power modes to ensure the sleep + // strobe works, but this overrides the user's "sleep + // backlight off" preference. By writing the actual + // user config here (after the kernel prepare callback + // has already run), we restore the user's intent while + // still allowing the kernel's brightness re-assertion + // to have set up the EC correctly for the strobe case. + let config = inner1.config.lock().await; + let sleep_enabled = config.enabled.states.iter() + .any(|s| s.zone == rog_aura::PowerZones::Keyboard && s.sleep); + drop(config); + + if !sleep_enabled { + info!("CtrlKbdLedTask sleep: user disabled sleep backlight, re-writing power state"); + let config = inner1.config.lock().await; + if let Err(e) = inner1.set_power_states(&config).await { + error!("CtrlKbdLedTask sleep power state rewrite: {e}"); + } + } else { + info!("CtrlKbdLedTask sleep: sleep backlight enabled, no-op"); + } + } else { info!("CtrlKbdLedTask reloading brightness and modes"); + let (brightness, led_type) = { + let config = inner1.config.lock().await; + (config.brightness.into(), config.led_type) + }; if let Some(backlight) = &inner1.backlight { - backlight - .lock() - .await - .set_brightness(inner1.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if let Err(e) = backlight.lock().await.set_brightness(brightness) { + error!("CtrlKbdLedTask wake brightness: {e}"); + return; + } } let mut config = inner1.config.lock().await; - inner1 - .write_current_config_mode(&mut config) - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); - } else if sleeping { - inner1 - .update_config() - .await - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if let Err(e) = inner1.write_current_config_mode(&mut config).await { + error!("CtrlKbdLedTask wake mode: {e}"); + return; + } + if led_type.is_tuf_laptop() + && let Err(e) = inner1.set_power_states(&config).await + { + error!("CtrlKbdLedTask wake power state: {e}"); + } } } }, diff --git a/asusd/src/lib.rs b/asusd/src/lib.rs index 3789d76cd..3219f743f 100644 --- a/asusd/src/lib.rs +++ b/asusd/src/lib.rs @@ -435,14 +435,21 @@ pub trait CtrlTask { tokio::spawn({ let logind_manager = logind_manager.clone(); async move { - if let Ok(mut notif) = logind_manager.receive_prepare_for_sleep().await { - while let Some(event) = notif.next().await { - // blocks thread :| - if let Ok(args) = event.args() { - debug!("Doing on_prepare_for_sleep({})", args.start); - on_prepare_for_sleep(args.start).await; + match logind_manager.receive_prepare_for_sleep().await { + Ok(mut notif) => { + info!("Subscribed to logind PrepareForSleep"); + while let Some(event) = notif.next().await { + // blocks thread :| + if let Ok(args) = event.args() { + debug!("Doing on_prepare_for_sleep({})", args.start); + on_prepare_for_sleep(args.start).await; + } else { + warn!("Failed to decode logind PrepareForSleep signal"); + } } + warn!("logind PrepareForSleep stream ended"); } + Err(err) => warn!("Could not subscribe to logind PrepareForSleep: {err}"), } } }); diff --git a/rog-aura/data/aura_support.ron b/rog-aura/data/aura_support.ron index 31c4a4a28..f4366df56 100644 --- a/rog-aura/data/aura_support.ron +++ b/rog-aura/data/aura_support.ron @@ -1,4 +1,13 @@ ([ + ( + device_name: "FA401", + product_id: "", + layout_name: "fa507", + basic_modes: [Static, Breathe, Pulse], + basic_zones: [], + advanced_type: r#None, + power_zones: [Keyboard], + ), ( device_name: "FA506I", product_id: "", diff --git a/rog-aura/src/keyboard/power.rs b/rog-aura/src/keyboard/power.rs index 0b2bd70d3..d9c6357dd 100644 --- a/rog-aura/src/keyboard/power.rs +++ b/rog-aura/src/keyboard/power.rs @@ -339,6 +339,21 @@ mod test { ) } + #[test] + fn tuf_power_bytes_include_boot_awake_and_sleep() { + let power = LaptopAuraPower { + states: vec![AuraPowerState { + zone: PowerZones::Keyboard, + boot: true, + awake: false, + sleep: true, + shutdown: false, + }], + }; + + assert_eq!(power.to_bytes(AuraDeviceType::LaptopKeyboardTuf), vec![1, 1, 0, 1, 1]); + } + #[test] fn check_0x1866_control_bytes() { let power = LaptopAuraPower { From 5aaefe348382e09a4e4f0e3e5fb05b5e196a07d3 Mon Sep 17 00:00:00 2001 From: ido <106871003+idobutidont@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:58:17 +0700 Subject: [PATCH 2/2] fix(rog-aura): refactor TUF keyboard power state handling and improve sleep strobe brightness management --- asusctl/src/main.rs | 51 ++++++++++++++++---------- asusd/src/aura_laptop/trait_impls.rs | 53 +++++++++++++--------------- rog-aura/src/keyboard/power.rs | 23 +++++++----- 3 files changed, 71 insertions(+), 56 deletions(-) diff --git a/asusctl/src/main.rs b/asusctl/src/main.rs index 96811a50d..b0d1c40b9 100644 --- a/asusctl/src/main.rs +++ b/asusctl/src/main.rs @@ -668,26 +668,39 @@ fn handle_led_power_1_do_1866( power: &LedPowerCommand1, ) -> Result<(), Box> { let mut states = Vec::new(); - // TUF exposes one keyboard zone; the generic 0x1866 command may expose - // separate keyboard and lightbar zones. let is_tuf = aura.device_type()?.is_tuf_laptop(); - if power.keyboard || (is_tuf && power.lightbar) { - states.push(AuraPowerState { - zone: PowerZones::Keyboard, - boot: power.boot.unwrap_or_default(), - awake: power.awake.unwrap_or_default(), - sleep: power.sleep.unwrap_or_default(), - shutdown: false, - }); - } - if power.lightbar && !is_tuf { - states.push(AuraPowerState { - zone: PowerZones::Lightbar, - boot: power.boot.unwrap_or_default(), - awake: power.awake.unwrap_or_default(), - sleep: power.sleep.unwrap_or_default(), - shutdown: false, - }); + + // TUF laptops have only a keyboard zone; lightbar does not exist. + if is_tuf { + if power.keyboard || power.lightbar { + states.push(AuraPowerState { + zone: PowerZones::Keyboard, + boot: power.boot.unwrap_or_default(), + awake: power.awake.unwrap_or_default(), + sleep: power.sleep.unwrap_or_default(), + shutdown: false, + }); + } + } else { + // Pre-2021 0x1866 keyboards may have separate keyboard and lightbar zones. + if power.keyboard { + states.push(AuraPowerState { + zone: PowerZones::Keyboard, + boot: power.boot.unwrap_or_default(), + awake: power.awake.unwrap_or_default(), + sleep: power.sleep.unwrap_or_default(), + shutdown: false, + }); + } + if power.lightbar { + states.push(AuraPowerState { + zone: PowerZones::Lightbar, + boot: power.boot.unwrap_or_default(), + awake: power.awake.unwrap_or_default(), + sleep: power.sleep.unwrap_or_default(), + shutdown: false, + }); + } } let states = LaptopAuraPower { states }; diff --git a/asusd/src/aura_laptop/trait_impls.rs b/asusd/src/aura_laptop/trait_impls.rs index fa9cc7817..5e00daee4 100644 --- a/asusd/src/aura_laptop/trait_impls.rs +++ b/asusd/src/aura_laptop/trait_impls.rs @@ -256,28 +256,29 @@ impl CtrlTask for AuraZbus { async move { info!("CtrlKbdLedTask received prepare_for_sleep({sleeping})"); if sleeping { - // Re-write the user's configured power state right - // before suspend. The kernel patch re-asserts brightness - // and enables all power modes to ensure the sleep - // strobe works, but this overrides the user's "sleep - // backlight off" preference. By writing the actual - // user config here (after the kernel prepare callback - // has already run), we restore the user's intent while - // still allowing the kernel's brightness re-assertion - // to have set up the EC correctly for the strobe case. + let (sleep_enabled, led_type, user_brightness) = { + let config = inner1.config.lock().await; + ( + config.enabled.states.iter() + .any(|s| s.zone == rog_aura::PowerZones::Keyboard && s.sleep), + config.led_type, + u8::from(config.brightness), + ) + }; + let config = inner1.config.lock().await; - let sleep_enabled = config.enabled.states.iter() - .any(|s| s.zone == rog_aura::PowerZones::Keyboard && s.sleep); - drop(config); + if let Err(e) = inner1.set_power_states(&config).await { + error!("CtrlKbdLedTask sleep power state write: {e}"); + } - if !sleep_enabled { - info!("CtrlKbdLedTask sleep: user disabled sleep backlight, re-writing power state"); - let config = inner1.config.lock().await; - if let Err(e) = inner1.set_power_states(&config).await { - error!("CtrlKbdLedTask sleep power state rewrite: {e}"); + if sleep_enabled && led_type.is_tuf_laptop() { + info!("CtrlKbdLedTask sleep: TUF sleep strobe requires non-zero brightness"); + if let Some(backlight) = &inner1.backlight { + let target_brightness: u8 = if user_brightness == 0 { 3 } else { user_brightness }; + if let Err(e) = backlight.lock().await.set_brightness(target_brightness) { + error!("CtrlKbdLedTask sleep brightness: {e}"); + } } - } else { - info!("CtrlKbdLedTask sleep: sleep backlight enabled, no-op"); } } else { info!("CtrlKbdLedTask reloading brightness and modes"); @@ -308,17 +309,11 @@ impl CtrlTask for AuraZbus { let inner3 = inner3.clone(); async move { info!("CtrlKbdLedTask reloading brightness and modes"); + let brightness = inner3.config.lock().await.brightness.into(); if let Some(backlight) = &inner3.backlight { - // unwrap as we want to bomb out of the task - backlight - .lock() - .await - .set_brightness(inner3.config.lock().await.brightness.into()) - .map_err(|e| { - error!("CtrlKbdLedTask: {e}"); - e - }) - .unwrap(); + if let Err(e) = backlight.lock().await.set_brightness(brightness) { + error!("CtrlKbdLedTask: {e}"); + } } } }, diff --git a/rog-aura/src/keyboard/power.rs b/rog-aura/src/keyboard/power.rs index d9c6357dd..7d68984fe 100644 --- a/rog-aura/src/keyboard/power.rs +++ b/rog-aura/src/keyboard/power.rs @@ -342,16 +342,23 @@ mod test { #[test] fn tuf_power_bytes_include_boot_awake_and_sleep() { let power = LaptopAuraPower { - states: vec![AuraPowerState { - zone: PowerZones::Keyboard, - boot: true, - awake: false, - sleep: true, - shutdown: false, - }], + states: vec![ + AuraPowerState { + zone: PowerZones::Keyboard, + boot: true, + awake: false, + sleep: true, + shutdown: false, + }, + ], }; - assert_eq!(power.to_bytes(AuraDeviceType::LaptopKeyboardTuf), vec![1, 1, 0, 1, 1]); + assert_eq!( + power.to_bytes(AuraDeviceType::LaptopKeyboardTuf), + vec![ + 1, 1, 0, 1, 1 + ] + ); } #[test]