Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,113 changes: 1,113 additions & 0 deletions ARMOURY_CRATE_HARDWARE_PROTOCOL_MAP.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ ron = "*"
log = "^0.4"
env_logger = "^0.10.0"
thiserror = "2"
libc = "0.2"

glam = { version = "^0.22", features = ["serde"] }
gumdrop = "^0.8"
Expand Down
61 changes: 56 additions & 5 deletions asusd/src/aura_laptop/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,33 @@ impl StdConfigLoad for AuraConfig {}
impl AuraConfig {
/// Detect the keyboard type and load from default DB if data available
pub fn new(prod_id: &str) -> Self {
Self::new_with_modes(prod_id, None)
}

/// Create the default config, optionally replacing the static mode list
/// with a validated firmware report. The firmware list is authoritative
/// only after the HID decoder has checked its header and required Static
/// bit; callers can therefore pass `None` for older keyboards.
pub fn new_with_modes(prod_id: &str, detected_modes: Option<&[AuraModeNum]>) -> Self {
info!("Setting up AuraConfig for {prod_id:?}");
// create a default config here
let device_type = AuraDeviceType::from(prod_id);
if device_type == AuraDeviceType::Unknown {
warn!("idProduct:{prod_id:?} is unknown");
}
let support_data = LedSupportData::get_data(prod_id);
let mut support_data = LedSupportData::get_data(prod_id);
if let Some(detected_modes) = detected_modes {
let mut modes = detected_modes.to_vec();
modes.sort();
modes.dedup();
if modes.contains(&AuraModeNum::Static) {
info!(
"Using {} firmware-reported Aura modes for {prod_id:?}",
modes.len()
);
support_data.basic_modes = modes;
}
}
let enabled = LaptopAuraPower::new(device_type, &support_data);
let mut config = AuraConfig {
led_type: device_type,
Expand Down Expand Up @@ -176,8 +196,16 @@ impl AuraConfig {
/// Reload the config from disk then verify and update it if required.
/// Always rewrites the file to disk.
pub fn load_and_update_config(prod_id: &str) -> AuraConfig {
Self::load_and_update_config_with_modes(prod_id, None)
}

/// Reload the config while retaining a validated firmware mode list.
pub fn load_and_update_config_with_modes(
prod_id: &str,
detected_modes: Option<&[AuraModeNum]>,
) -> AuraConfig {
// New loads data from the DB also
let mut config_init = AuraConfig::new(prod_id);
let mut config_init = AuraConfig::new_with_modes(prod_id, detected_modes);
// config_init.set_filename(prod_id);
let mut config_loaded = config_init.clone().load();
// update the initialised data with what we loaded from disk
Expand All @@ -189,10 +217,19 @@ impl AuraConfig {
}
// Then replace just incase the initialised data contains new modes added
config_loaded.builtins = config_init.builtins;
config_loaded.support_data = config_init.support_data;
config_loaded.support_data = config_init.support_data.clone();
config_loaded.led_type = config_init.led_type;
config_loaded.ally_fix = config_init.ally_fix;

// A mode removed by a firmware readback must not remain selected from
// an older on-disk configuration.
if !config_loaded
.builtins
.contains_key(&config_loaded.current_mode)
{
config_loaded.current_mode = AuraModeNum::Static;
}

for enabled_init in &mut config_init.enabled.states {
for enabled in &mut config_loaded.enabled.states {
if enabled.zone == enabled_init.zone {
Expand All @@ -210,10 +247,9 @@ impl AuraConfig {
// update init values from loaded values if they exist
if let Some(loaded) = multizone_loaded.get(mode.0) {
let mut new_set = Vec::new();
let data = LedSupportData::get_data(prod_id);
// only reuse a zone mode if the mode is supported
for mode in loaded {
if data.basic_modes.contains(&mode.mode) {
if config_init.support_data.basic_modes.contains(&mode.mode) {
new_set.push(mode.clone());
}
}
Expand Down Expand Up @@ -464,4 +500,19 @@ mod tests {
}
);
}

#[test]
fn firmware_modes_replace_static_mode_table() {
let _guard = test_lock();
std::env::set_var("BOARD_NAME", "GV601VV");
let modes = [
AuraModeNum::Static,
AuraModeNum::RainbowCycle,
];
let config = AuraConfig::new_with_modes("19b6", Some(&modes));

assert_eq!(config.support_data.basic_modes, modes);
assert!(config.builtins.contains_key(&AuraModeNum::RainbowCycle));
assert!(!config.builtins.contains_key(&AuraModeNum::Pulse));
}
}
96 changes: 78 additions & 18 deletions asusd/src/aura_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
// - If udev sees device removed then remove the zbus path

use std::collections::{HashMap, HashSet};
use std::fs;
use std::sync::Arc;

use dmi_id::DMIID;
use log::{debug, error, info, warn};
use mio::{Events, Interest, Poll, Token};
use rog_aura::AuraDeviceType;
use rog_platform::error::PlatformError;
use rog_platform::hid_raw::HidRaw;
use rog_platform::hid_raw::{feature_report_len, HidRaw};
use tokio::sync::Mutex;
use udev::{Device, MonitorBuilder};
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
Expand Down Expand Up @@ -85,6 +87,42 @@ fn dev_prop_matches(dev: &Device, prop: &str, value: &str) -> bool {
false
}

/// Return whether a hidraw endpoint advertises the Aura feature report.
///
/// ASUS keyboards commonly expose several HID interfaces under one USB
/// product ID (keyboard input, vendor control, mouse/gamepad, and Aura). The
/// first interface is not a stable contract, so the manager prefers the one
/// whose report descriptor contains report ID `0x5d` before applying the
/// existing one-interface-per-USB deduplication.
fn has_feature_report_id(device: &Device, report_id: u8) -> bool {
let Some(hid_device) = device.parent() else {
return false;
};
let descriptor = hid_device.syspath().join("report_descriptor");
fs::read(descriptor)
.map(|bytes| descriptor_has_feature_report(&bytes, report_id))
.unwrap_or(false)
}

fn descriptor_has_feature_report(bytes: &[u8], report_id: u8) -> bool {
feature_report_len(bytes, report_id).is_some()
}

fn is_modern_aura_endpoint(device: &Device) -> bool {
let Ok(Some(usb_device)) = device.parent_with_subsystem_devtype("usb", "usb_device") else {
return false;
};
if usb_device.attribute_value("idVendor") != Some(std::ffi::OsStr::new("0b05")) {
return false;
}
let Some(product_id) = usb_device.attribute_value("idProduct") else {
return false;
};

AuraDeviceType::from(product_id.to_str().unwrap_or_default()).is_new_laptop()
&& has_feature_report_id(device, 0x5d)
}

/// A device.
///
/// Each controller within should track its dbus path so it can be removed if
Expand Down Expand Up @@ -134,6 +172,17 @@ impl DeviceManager {
debug!("Not ASUS vendor ID: {}", vendor_id.to_string_lossy());
return Ok(devices);
}
let product_id = usb_id.to_str().unwrap_or_default();
// Startup enumeration is sorted below, but hotplug/resume
// events arrive one interface at a time. Do not let an
// input or gamepad interface register a modern Aura
// keyboard before its 0x5d endpoint appears.
if AuraDeviceType::from(product_id).is_new_laptop()
&& !has_feature_report_id(&device, 0x5d)
{
debug!("Skipping non-Aura hidraw interface for modern keyboard {usb_id:?}");
return Ok(devices);
}
// Almost all devices are identified by the productId.
// So let's see what we have and:
// 1. Generate an interface path
Expand All @@ -144,11 +193,8 @@ impl DeviceManager {
{
debug!("Testing device {usb_id:?}");
// SLASH DEVICE
if let Ok(dev_type) = DeviceHandle::new_slash_hid(
dev.clone(),
usb_id.to_str().unwrap_or_default(),
)
.await
if let Ok(dev_type) =
DeviceHandle::new_slash_hid(dev.clone(), product_id).await
{
if let DeviceHandle::Slash(slash) = dev_type.clone() {
let path =
Expand All @@ -163,11 +209,8 @@ impl DeviceManager {
}
}
// ANIME MATRIX DEVICE
if let Ok(dev_type) = DeviceHandle::maybe_anime_hid(
dev.clone(),
usb_id.to_str().unwrap_or_default(),
)
.await
if let Ok(dev_type) =
DeviceHandle::maybe_anime_hid(dev.clone(), product_id).await
{
if let DeviceHandle::AniMe(anime) = dev_type.clone() {
let path =
Expand All @@ -182,11 +225,8 @@ impl DeviceManager {
}
}
// AURA LAPTOP DEVICE
if let Ok(dev_type) = DeviceHandle::maybe_laptop_aura(
Some(dev),
usb_id.to_str().unwrap_or_default(),
)
.await
if let Ok(dev_type) =
DeviceHandle::maybe_laptop_aura(Some(dev), product_id).await
{
if let DeviceHandle::Aura(aura) = dev_type.clone() {
let path =
Expand Down Expand Up @@ -231,10 +271,16 @@ impl DeviceManager {
PlatformError::Udev("match_subsystem failed".into(), err)
})?;

for device in enumerator
let mut hid_devices: Vec<Device> = enumerator
.scan_devices()
.map_err(|e| PlatformError::IoPath("enumerator".to_owned(), e))?
{
.collect();
// Keep the udev order as a stable fallback, but process the Aura
// endpoint first for modern notebook controllers. Do not reorder
// unrelated ASUS peripherals which may reuse report ID 0x5d.
hid_devices.sort_by_key(|device| !is_modern_aura_endpoint(device));

for device in hid_devices {
// Only deduplicate ASUS devices; non-ASUS multi-interface devices are unaffected.
if let Ok(Some(usb_parent)) = device.parent_with_subsystem_devtype("usb", "usb_device")
{
Expand Down Expand Up @@ -631,3 +677,17 @@ impl DeviceManager {
Ok(manager)
}
}

#[cfg(test)]
mod tests {
use super::descriptor_has_feature_report;

#[test]
fn selects_aura_report_id_from_descriptor() {
let descriptor = [
0x85, 0x5d, 0x75, 0x08, 0x95, 0x3f, 0xb1, 0x00,
];
assert!(descriptor_has_feature_report(&descriptor, 0x5d));
assert!(!descriptor_has_feature_report(&descriptor, 0x01));
}
}
69 changes: 67 additions & 2 deletions asusd/src/aura_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use log::{debug, error, info};
use rog_anime::error::AnimeError;
use rog_anime::usb::get_anime_type;
use rog_anime::AnimeType;
use rog_aura::AuraDeviceType;
use rog_aura::aura_capabilities::{
parse_firmware_capabilities, parse_hardware_status, parse_power_state_readback,
};
use rog_aura::{AuraDeviceType, AuraModeNum};
use rog_platform::hid_raw::HidRaw;
use rog_platform::keyboard_led::KeyboardBacklight;
use rog_platform::usb_raw::USBRaw;
Expand Down Expand Up @@ -197,8 +200,70 @@ impl DeviceHandle {
Some(Arc::new(Mutex::new(k)))
});

// Modern 0x19b6/0x1a30 keyboards expose their real mode table and
// physical controller capabilities through read-only feature
// reports. Probe before loading the config so newly discovered modes
// become ordinary D-Bus modes and retain saved colours where present.
let mut detected_modes: Option<Vec<AuraModeNum>> = None;
if aura_type.is_new_laptop() {
if let Some(hid) = device.as_ref() {
let hid = hid.lock().await;
match hid.query_aura_status_report() {
Ok(Some(report)) => {
if let Some(status) = parse_hardware_status(&report) {
info!(
"Aura firmware status: type={:#04x} year={:#04x} layout={:#04x} regions={:#04x} features={:#04x} family={:#04x}",
status.keyboard_type,
status.keyboard_year,
status.layout,
status.region_bits,
status.feature_bits,
status.model_family
);
} else {
log::debug!("Aura firmware status response failed validation");
}
}
Ok(None) => log::debug!("Aura firmware status report is unavailable"),
Err(error) => log::debug!("Aura firmware status probe failed: {error}"),
}

match hid.query_aura_capability_report() {
Ok(Some(report)) => {
if let Some(capabilities) = parse_firmware_capabilities(&report) {
info!(
"Aura firmware modes: {:?} (mask={:#04x}{:#04x})",
capabilities.modes,
capabilities.mode_mask_high,
capabilities.mode_mask_low
);
if let Some(power) = parse_power_state_readback(&report) {
info!(
"Aura firmware power readback: keyboard={:#04x} logo={:#04x} lightbar={:#04x} aero={:#04x} vcut={:#04x} bump={:#04x} rearglow={:#04x} ac_dc={:?}",
power.keyboard,
power.logo,
power.lightbar,
power.aero,
power.vcut,
power.bump,
power.rear_glow,
power.awake_ac_dc
);
}
detected_modes = Some(capabilities.modes);
} else {
log::debug!("Aura firmware capability response failed validation");
}
}
Ok(None) => log::debug!("Aura firmware capability report is unavailable"),
Err(error) => log::debug!("Aura firmware capability probe failed: {error}"),
}
}
}

// Load saved mode, colours, brightness, power from disk; apply on reload
let mut config = AuraConfig::load_and_update_config(prod_id);
let mut config =
AuraConfig::load_and_update_config_with_modes(prod_id, detected_modes.as_deref());
config.led_type = aura_type;
let aura = Aura {
hid: device,
Expand Down
17 changes: 13 additions & 4 deletions rog-aura/data/aura_support.ron
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,19 @@
power_zones: [Keyboard],
),
(
device_name: "FA608WI",
device_name: "FA608UH",
product_id: "",
layout_name: "fa608",
basic_modes: [Static, Breathe, RainbowCycle, Pulse],
basic_modes: [Static, Breathe, RainbowCycle, RainbowWave, Pulse, Flash],
basic_zones: [],
advanced_type: r#None,
power_zones: [Keyboard],
),
(
device_name: "FA608UH",
device_name: "FA608WI",
product_id: "",
layout_name: "fa608",
basic_modes: [Static, Breathe, RainbowCycle, RainbowWave, Pulse, Flash],
basic_modes: [Static, Breathe, RainbowCycle, Pulse],
basic_zones: [],
advanced_type: r#None,
power_zones: [Keyboard],
Expand Down Expand Up @@ -1070,6 +1070,15 @@
advanced_type: Zoned([SingleZone]),
power_zones: [Keyboard],
),
(
device_name: "GV601VV",
product_id: "",
layout_name: "ga401q",
basic_modes: [Static, Breathe, RainbowCycle, Pulse],
basic_zones: [],
advanced_type: Zoned([SingleZone]),
power_zones: [Keyboard],
),
(
device_name: "GV604V",
product_id: "",
Expand Down
Loading