diff --git a/Cargo.lock b/Cargo.lock index c3e4811e..1c89b48e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,7 @@ dependencies = [ "rog_profiles", "rog_types", "serde_json", + "supergfxctl", ] [[package]] @@ -54,6 +55,7 @@ dependencies = [ "rog_dbus", "rog_profiles", "rog_types", + "supergfxctl", "tinybmp", "yansi-term", ] @@ -219,6 +221,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "supergfxctl", "sysfs-class", "toml", "udev", @@ -922,6 +925,7 @@ dependencies = [ "rog_aura", "rog_profiles", "rog_types", + "supergfxctl", "zbus", "zbus_macros", "zvariant", @@ -1062,6 +1066,21 @@ dependencies = [ "syn 0.11.11", ] +[[package]] +name = "supergfxctl" +version = "1.1.0" +dependencies = [ + "log", + "logind-zbus", + "serde", + "serde_derive", + "serde_json", + "sysfs-class", + "zbus", + "zvariant", + "zvariant_derive", +] + [[package]] name = "syn" version = "0.11.11" diff --git a/Cargo.toml b/Cargo.toml index 774d9f13..e4983797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["asusctl", "asus-notify", "daemon", "daemon-user", "rog-types", "rog-dbus", "rog-anime", "rog-aura", "rog-profiles"] +members = ["asusctl", "asus-notify", "daemon", "daemon-user", "rog-types", "rog-dbus", "rog-anime", "rog-aura", "rog-profiles", "supergfx"] [profile.release] lto = true @@ -13,4 +13,4 @@ opt-level = 1 [profile.bench] debug = false -opt-level = 3 \ No newline at end of file +opt-level = 3 diff --git a/asus-notify/Cargo.toml b/asus-notify/Cargo.toml index 251708a2..7f5bc0f6 100644 --- a/asus-notify/Cargo.toml +++ b/asus-notify/Cargo.toml @@ -13,6 +13,7 @@ rog_dbus = { path = "../rog-dbus" } rog_aura = { path = "../rog-aura" } rog_types = { path = "../rog-types" } rog_profiles = { path = "../rog-profiles" } +supergfxctl = { path = "../supergfx" } [dependencies.notify-rust] version = "^4.3" diff --git a/asus-notify/src/main.rs b/asus-notify/src/main.rs index 83f96401..e36c75f8 100644 --- a/asus-notify/src/main.rs +++ b/asus-notify/src/main.rs @@ -2,8 +2,7 @@ use notify_rust::{Hint, Notification, NotificationHandle}; use rog_aura::AuraEffect; use rog_dbus::{DbusProxies, Signals}; use rog_profiles::Profile; -use rog_types::gfx_vendors::GfxRequiredUserAction; -use rog_types::gfx_vendors::GfxVendors; +use supergfxctl::gfx_vendors::{GfxRequiredUserAction, GfxVendors}; use std::error::Error; use std::process; use std::thread::sleep; diff --git a/asusctl/Cargo.toml b/asusctl/Cargo.toml index c84f1d2b..1e1cce13 100644 --- a/asusctl/Cargo.toml +++ b/asusctl/Cargo.toml @@ -15,6 +15,7 @@ rog_types = { path = "../rog-types" } daemon = { path = "../daemon" } gumdrop = "^0.8" yansi-term = "^0.1" +supergfxctl = { path = "../supergfx" } [dev-dependencies] tinybmp = "^0.2.3" diff --git a/asusctl/src/main.rs b/asusctl/src/main.rs index 6e41a292..bdfc6ca8 100644 --- a/asusctl/src/main.rs +++ b/asusctl/src/main.rs @@ -10,12 +10,12 @@ use rog_anime::{AnimeDataBuffer, AnimeImage, Vec2, ANIME_DATA_LEN}; use rog_aura::{self, AuraEffect}; use rog_dbus::RogDbusClient; use rog_types::{ - gfx_vendors::{GfxRequiredUserAction, GfxVendors}, supported::{ AnimeSupportedFunctions, LedSupportedFunctions, PlatformProfileFunctions, RogBiosSupportedFunctions, }, }; +use supergfxctl::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; use std::{env::args, path::Path}; use yansi_term::Colour::Green; use yansi_term::Colour::Red; @@ -240,7 +240,7 @@ fn do_gfx( if command.pow { let res = dbus.proxies().gfx().gfx_get_pwr()?; match res { - rog_types::gfx_vendors::GfxPower::Active => { + GfxPower::Active => { println!("Current power status: {}", Red.paint(<&str>::from(&res))) } _ => println!("Current power status: {}", Green.paint(<&str>::from(&res))), diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 60e167f8..e6addacd 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -23,6 +23,7 @@ rog_aura = { path = "../rog-aura" } rog_types = { path = "../rog-types" } rog_profiles = { path = "../rog-profiles" } rog_dbus = { path = "../rog-dbus" } +supergfxctl = { path = "../supergfx" } rusb = "^0.8" udev = "^0.6" diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 5d7c73c3..260e34b8 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -1,5 +1,4 @@ use log::{error, info, warn}; -use rog_types::gfx_vendors::GfxVendors; use serde_derive::{Deserialize, Serialize}; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; @@ -8,33 +7,20 @@ use crate::config_old::*; use crate::VERSION; pub static CONFIG_PATH: &str = "/etc/asusd/asusd.conf"; -pub static AURA_CONFIG_PATH: &str = "/etc/asusd/asusd.conf"; #[derive(Deserialize, Serialize)] pub struct Config { - pub gfx_mode: GfxVendors, - /// Only for informational purposes. - #[serde(skip)] - pub gfx_tmp_mode: Option, - pub gfx_managed: bool, - pub gfx_vfio_enable: bool, /// Save charge limit for restoring on boot pub bat_charge_limit: u8, } -impl Default for Config { - fn default() -> Self { +impl Config { + fn new() -> Self { Config { - gfx_mode: GfxVendors::Hybrid, - gfx_tmp_mode: None, - gfx_managed: true, - gfx_vfio_enable: false, bat_charge_limit: 100, } } -} -impl Config { /// `load` will attempt to read the config, and panic if the dir is missing pub fn load() -> Self { let mut file = OpenOptions::new() @@ -44,47 +30,23 @@ impl Config { .open(&CONFIG_PATH) .unwrap_or_else(|_| panic!("The directory /etc/asusd/ is missing")); // okay to cause panic here let mut buf = String::new(); + let config; if let Ok(read_len) = file.read_to_string(&mut buf) { if read_len == 0 { - return Config::create_default(&mut file); + config = Self::new(); + } else if let Ok(data) = serde_json::from_str(&buf) { + config = data; + } else if let Ok(data) = serde_json::from_str::(&buf) { + config = data.into_current(); + info!("Updated config version to: {}", VERSION); } else { - if let Ok(data) = serde_json::from_str(&buf) { - return data; - } else if let Ok(data) = serde_json::from_str::(&buf) { - let config = data.into_current(); - config.write(); - info!("Updated config version to: {}", VERSION); - return config; - } else if let Ok(data) = serde_json::from_str::(&buf) { - let config = data.into_current(); - config.write(); - info!("Updated config version to: {}", VERSION); - return config; - } else if let Ok(data) = serde_json::from_str::(&buf) { - let config = data.into_current(); - config.write(); - info!("Updated config version to: {}", VERSION); - return config; - } else if let Ok(data) = serde_json::from_str::(&buf) { - let config = data.into_current(); - config.write(); - info!("Updated config version to: {}", VERSION); - return config; - } warn!("Could not deserialise {}", CONFIG_PATH); panic!("Please remove {} then restart asusd", CONFIG_PATH); } + } else { + config = Self::new() } - Config::create_default(&mut file) - } - - fn create_default(file: &mut File) -> Self { - let config = Config::default(); - - // Should be okay to unwrap this as is since it is a Default - let json = serde_json::to_string_pretty(&config).unwrap(); - file.write_all(json.as_bytes()) - .unwrap_or_else(|_| panic!("Could not write {}", CONFIG_PATH)); + config.write(); config } @@ -98,11 +60,8 @@ impl Config { if l == 0 { warn!("File is empty {}", CONFIG_PATH); } else { - let mut x: Config = serde_json::from_str(&buf) - .unwrap_or_else(|_| panic!("Could not deserialise {}", CONFIG_PATH)); - // copy over serde skipped values - x.gfx_tmp_mode = self.gfx_tmp_mode; - *self = x; + serde_json::from_str(&buf) + .unwrap_or_else(|_| panic!("Could not deserialise {}", CONFIG_PATH)) } } } diff --git a/daemon/src/config_old.rs b/daemon/src/config_old.rs index 49254b3e..0271380b 100644 --- a/daemon/src/config_old.rs +++ b/daemon/src/config_old.rs @@ -1,88 +1,9 @@ -use rog_types::gfx_vendors::GfxVendors; use serde_derive::{Deserialize, Serialize}; +use supergfxctl::gfx_vendors::GfxVendors; use std::collections::BTreeMap; use crate::config::Config; -/// for parsing old v3.1.7 config -#[allow(dead_code)] -#[derive(Deserialize)] -pub(crate) struct ConfigV317 { - pub gfx_mode: GfxVendors, - pub gfx_managed: bool, - pub active_profile: String, - pub toggle_profiles: Vec, - #[serde(skip)] - pub curr_fan_mode: u8, - pub bat_charge_limit: u8, - pub kbd_led_brightness: u8, - pub kbd_backlight_mode: u8, - #[serde(skip)] - pub kbd_backlight_modes: Option, - pub power_profiles: BTreeMap, -} - -impl ConfigV317 { - pub(crate) fn into_current(self) -> Config { - Config { - gfx_mode: self.gfx_mode, - gfx_tmp_mode: None, - gfx_managed: self.gfx_managed, - gfx_vfio_enable: false, - bat_charge_limit: self.bat_charge_limit, - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct ConfigV324 { - pub gfx_mode: GfxVendors, - pub gfx_managed: bool, - pub active_profile: String, - pub toggle_profiles: Vec, - #[serde(skip)] - pub curr_fan_mode: u8, - pub bat_charge_limit: u8, - pub power_profiles: BTreeMap, -} - -impl ConfigV324 { - pub(crate) fn into_current(self) -> Config { - Config { - gfx_mode: GfxVendors::Hybrid, - gfx_tmp_mode: None, - gfx_managed: self.gfx_managed, - gfx_vfio_enable: false, - bat_charge_limit: self.bat_charge_limit, - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct ConfigV341 { - pub gfx_mode: GfxVendors, - pub gfx_managed: bool, - pub gfx_vfio_enable: bool, - pub active_profile: String, - pub toggle_profiles: Vec, - #[serde(skip)] - pub curr_fan_mode: u8, - pub bat_charge_limit: u8, - pub power_profiles: BTreeMap, -} - -impl ConfigV341 { - pub(crate) fn into_current(self) -> Config { - Config { - gfx_mode: GfxVendors::Hybrid, - gfx_tmp_mode: None, - gfx_managed: self.gfx_managed, - gfx_vfio_enable: false, - bat_charge_limit: self.bat_charge_limit, - } - } -} - #[derive(Deserialize, Serialize)] pub struct ConfigV352 { pub gfx_mode: GfxVendors, @@ -101,10 +22,6 @@ pub struct ConfigV352 { impl ConfigV352 { pub(crate) fn into_current(self) -> Config { Config { - gfx_mode: GfxVendors::Hybrid, - gfx_tmp_mode: None, - gfx_managed: self.gfx_managed, - gfx_vfio_enable: false, bat_charge_limit: self.bat_charge_limit, } } diff --git a/daemon/src/ctrl_anime/config.rs b/daemon/src/ctrl_anime/config.rs new file mode 100644 index 00000000..e189ad8e --- /dev/null +++ b/daemon/src/ctrl_anime/config.rs @@ -0,0 +1,257 @@ +use crate::VERSION; +use log::{error, info, warn}; +use rog_anime::Fade; +use rog_anime::{error::AnimeError, ActionData, ActionLoader, AnimTime, Vec2}; +use serde_derive::{Deserialize, Serialize}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::time::Duration; + +pub static ANIME_CONFIG_PATH: &str = "/etc/asusd/anime.conf"; +pub static ANIME_CACHE_PATH: &str = "/etc/asusd/anime-cache.conf"; + +#[derive(Deserialize, Serialize)] +pub struct AnimeConfigV341 { + pub system: Option, + pub boot: Option, + pub suspend: Option, + pub shutdown: Option, +} + +impl AnimeConfigV341 { + pub(crate) fn into_current(self) -> AnimeConfig { + AnimeConfig { + system: if let Some(ani) = self.system { + vec![ani] + } else { + vec![] + }, + boot: if let Some(ani) = self.boot { + vec![ani] + } else { + vec![] + }, + wake: if let Some(ani) = self.suspend { + vec![ani] + } else { + vec![] + }, + shutdown: if let Some(ani) = self.shutdown { + vec![ani] + } else { + vec![] + }, + brightness: 1.0, + awake_enabled: true, + boot_anim_enabled: true, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AnimeConfigV352 { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub shutdown: Vec, + pub brightness: f32, +} + +impl AnimeConfigV352 { + pub(crate) fn into_current(self) -> AnimeConfig { + AnimeConfig { + system: self.system, + boot: self.boot, + wake: self.wake, + shutdown: self.shutdown, + brightness: 1.0, + awake_enabled: true, + boot_anim_enabled: true, + } + } +} + +#[derive(Deserialize, Serialize, Default)] +pub struct AnimeConfigCached { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub shutdown: Vec, +} + +impl AnimeConfigCached { + pub fn init_from_config(&mut self, config: &AnimeConfig) -> Result<(), AnimeError> { + let mut sys = Vec::with_capacity(config.system.len()); + for ani in config.system.iter() { + sys.push(ActionData::from_anime_action(ani)?); + } + self.system = sys; + + let mut boot = Vec::with_capacity(config.boot.len()); + for ani in config.boot.iter() { + boot.push(ActionData::from_anime_action(ani)?); + } + self.boot = boot; + + let mut wake = Vec::with_capacity(config.wake.len()); + for ani in config.wake.iter() { + wake.push(ActionData::from_anime_action(ani)?); + } + self.wake = wake; + + let mut shutdown = Vec::with_capacity(config.shutdown.len()); + for ani in config.shutdown.iter() { + shutdown.push(ActionData::from_anime_action(ani)?); + } + self.shutdown = shutdown; + Ok(()) + } +} + +/// Config for base system actions for the anime display +#[derive(Deserialize, Serialize)] +pub struct AnimeConfig { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub shutdown: Vec, + pub brightness: f32, + pub awake_enabled: bool, + pub boot_anim_enabled: bool, +} + +impl Default for AnimeConfig { + fn default() -> Self { + AnimeConfig { + system: Vec::new(), + boot: Vec::new(), + wake: Vec::new(), + shutdown: Vec::new(), + brightness: 1.0, + awake_enabled: true, + boot_anim_enabled: true, + } + } +} + +impl AnimeConfig { + /// `load` will attempt to read the config, and panic if the dir is missing + pub fn load() -> Self { + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&ANIME_CONFIG_PATH) + .unwrap_or_else(|_| { + panic!( + "The file {} or directory /etc/asusd/ is missing", + ANIME_CONFIG_PATH + ) + }); // okay to cause panic here + let mut buf = String::new(); + if let Ok(read_len) = file.read_to_string(&mut buf) { + if read_len == 0 { + return AnimeConfig::create_default(&mut file); + } else { + if let Ok(data) = serde_json::from_str(&buf) { + return data; + } else if let Ok(data) = serde_json::from_str::(&buf) { + let config = data.into_current(); + config.write(); + info!("Updated config version to: {}", VERSION); + return config; + } else if let Ok(data) = serde_json::from_str::(&buf) { + let config = data.into_current(); + config.write(); + info!("Updated config version to: {}", VERSION); + return config; + } + AnimeConfig::write_backup(buf); + warn!( + "Could not deserialise {}. Backed up as *-old", + ANIME_CONFIG_PATH + ); + } + } + AnimeConfig::create_default(&mut file) + } + + fn create_default(file: &mut File) -> Self { + // create a default config here + let config = AnimeConfig { + system: vec![], + boot: vec![ActionLoader::ImageAnimation { + file: "/usr/share/asusd/anime/custom/sonic-run.gif".into(), + scale: 0.9, + angle: 0.65, + translation: Vec2::default(), + brightness: 1.0, + time: AnimTime::Fade(Fade::new( + Duration::from_secs(2), + Some(Duration::from_secs(2)), + Duration::from_secs(2), + )), + }], + wake: vec![ActionLoader::ImageAnimation { + file: "/usr/share/asusd/anime/custom/sonic-run.gif".into(), + scale: 0.9, + angle: 0.65, + translation: Vec2::default(), + brightness: 1.0, + time: AnimTime::Fade(Fade::new( + Duration::from_secs(2), + Some(Duration::from_secs(2)), + Duration::from_secs(2), + )), + }], + shutdown: vec![ActionLoader::ImageAnimation { + file: "/usr/share/asusd/anime/custom/sonic-wait.gif".into(), + scale: 0.9, + angle: 0.0, + translation: Vec2::new(3.0, 2.0), + brightness: 1.0, + time: AnimTime::Infinite, + }], + brightness: 1.0, + awake_enabled: true, + boot_anim_enabled: true, + }; + // Should be okay to unwrap this as is since it is a Default + let json = serde_json::to_string_pretty(&config).unwrap(); + file.write_all(json.as_bytes()) + .unwrap_or_else(|_| panic!("Could not write {}", ANIME_CONFIG_PATH)); + config + } + + pub fn read(&mut self) { + let mut file = OpenOptions::new() + .read(true) + .open(&ANIME_CONFIG_PATH) + .unwrap_or_else(|err| panic!("Error reading {}: {}", ANIME_CONFIG_PATH, err)); + let mut buf = String::new(); + if let Ok(l) = file.read_to_string(&mut buf) { + if l == 0 { + warn!("File is empty {}", ANIME_CONFIG_PATH); + } else { + let x: AnimeConfig = serde_json::from_str(&buf) + .unwrap_or_else(|_| panic!("Could not deserialise {}", ANIME_CONFIG_PATH)); + *self = x; + } + } + } + + pub fn write(&self) { + let mut file = File::create(ANIME_CONFIG_PATH).expect("Couldn't overwrite config"); + let json = serde_json::to_string_pretty(self).expect("Parse config to JSON failed"); + file.write_all(json.as_bytes()) + .unwrap_or_else(|err| error!("Could not write config: {}", err)); + } + + fn write_backup(buf: String) { + let mut path = ANIME_CONFIG_PATH.to_string(); + path.push_str("-old"); + let mut file = File::create(&path).expect("Couldn't overwrite config"); + file.write_all(buf.as_bytes()) + .unwrap_or_else(|err| error!("Could not write config: {}", err)); + } +} diff --git a/daemon/src/ctrl_anime/mod.rs b/daemon/src/ctrl_anime/mod.rs new file mode 100644 index 00000000..61c603a0 --- /dev/null +++ b/daemon/src/ctrl_anime/mod.rs @@ -0,0 +1,329 @@ +pub mod config; +pub mod zbus; + +use ::zbus::Connection; +use log::{error, info, warn}; +use logind_zbus::ManagerProxy; +use rog_anime::{ + usb::{ + pkt_for_apply, pkt_for_flush, pkt_for_set_boot, pkt_for_set_on, pkts_for_init, PROD_ID, + VENDOR_ID, + }, + ActionData, AnimeDataBuffer, AnimePacketType, ANIME_DATA_LEN, +}; +use rog_types::supported::AnimeSupportedFunctions; +use rusb::{Device, DeviceHandle}; +use std::{ + error::Error, + sync::{Arc, Mutex}, + thread::sleep, +}; +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + +use crate::{error::RogError, GetSupported}; + +use self::config::{AnimeConfig, AnimeConfigCached}; + +impl GetSupported for CtrlAnime { + type A = AnimeSupportedFunctions; + + fn get_supported() -> Self::A { + AnimeSupportedFunctions(CtrlAnime::get_device(VENDOR_ID, PROD_ID).is_ok()) + } +} + +pub struct CtrlAnime { + handle: DeviceHandle, + cache: AnimeConfigCached, + config: AnimeConfig, + // set to force thread to exit + thread_exit: Arc, + // Set to false when the thread exits + thread_running: Arc, +} + +impl CtrlAnime { + #[inline] + pub fn new(config: AnimeConfig) -> Result> { + // We don't expect this ID to ever change + let device = CtrlAnime::get_device(0x0b05, 0x193b)?; + + let mut device = device.open()?; + device.reset()?; + + device.set_auto_detach_kernel_driver(true).map_err(|err| { + error!("Auto-detach kernel driver failed: {}", err); + err + })?; + + device.claim_interface(0).map_err(|err| { + error!("Could not claim device interface: {}", err); + err + })?; + + info!("Device has an AniMe Matrix display"); + let mut cache = AnimeConfigCached::default(); + cache.init_from_config(&config)?; + + let ctrl = CtrlAnime { + handle: device, + cache, + config, + thread_exit: Arc::new(AtomicBool::new(false)), + thread_running: Arc::new(AtomicBool::new(false)), + }; + ctrl.do_initialization(); + + Ok(ctrl) + } + + fn get_device(vendor: u16, product: u16) -> Result, rusb::Error> { + for device in rusb::devices()?.iter() { + let device_desc = device.device_descriptor()?; + if device_desc.vendor_id() == vendor && device_desc.product_id() == product { + return Ok(device); + } + } + Err(rusb::Error::NoDevice) + } + + /// Start an action thread. This is classed as a singleton and there should be only + /// one running - so the thread uses atomics to signal run/exit. + /// + /// Because this also writes to the usb device, other write tries (display only) *must* + /// get the mutex lock and set the thread_exit atomic. + fn run_thread(inner: Arc>, actions: Vec, mut once: bool) { + if actions.is_empty() { + warn!("AniMe system actions was empty"); + return; + } + // Loop rules: + // - Lock the mutex **only when required**. That is, the lock must be held for the shortest duration possible. + // - An AtomicBool used for thread exit should be checked in every loop, including nested + + // The only reason for this outer thread is to prevent blocking while waiting for the + // next spawned thread to exit + std::thread::Builder::new() + .name("AniMe system thread start".into()) + .spawn(move || { + info!("AniMe system thread started"); + // Getting copies of these Atomics is done *in* the thread to ensure + // we don't block other threads/main + let thread_exit; + let thread_running; + // First two loops are to ensure we *do* aquire a lock on the mutex + // The reason the loop is required is because the USB writes can block + // for up to 10ms. We can't fail to get the atomics. + loop { + if let Ok(lock) = inner.try_lock() { + thread_exit = lock.thread_exit.clone(); + thread_running = lock.thread_running.clone(); + // Make any running loop exit first + thread_exit.store(true, Ordering::SeqCst); + break; + } + } + + loop { + // wait for other threads to set not running so we know they exited + if !thread_running.load(Ordering::SeqCst) { + thread_exit.store(false, Ordering::SeqCst); + info!("AniMe forced a thread to exit"); + break; + } + } + + 'main: loop { + if thread_exit.load(Ordering::SeqCst) { + break 'main; + } + for action in actions.iter() { + match action { + ActionData::Animation(frames) => { + rog_anime::run_animation(frames, thread_exit.clone(), &|frame| { + if let Ok(lock) = inner.try_lock() { + lock.write_data_buffer(frame); + } + }) + .unwrap(); + + if thread_exit.load(Ordering::SeqCst) { + break 'main; + } + } + ActionData::Image(image) => { + once = false; + if let Ok(lock) = inner.try_lock() { + lock.write_data_buffer(image.as_ref().clone()) + } + } + ActionData::Pause(duration) => sleep(*duration), + ActionData::AudioEq => {} + ActionData::SystemInfo => {} + ActionData::TimeDate => {} + ActionData::Matrix => {} + } + } + if once || actions.is_empty() { + break 'main; + } + } + // Clear the display on exit + if let Ok(lock) = inner.try_lock() { + let data = AnimeDataBuffer::from_vec([0u8; ANIME_DATA_LEN].to_vec()); + lock.write_data_buffer(data); + } + // Loop ended, set the atmonics + thread_exit.store(false, Ordering::SeqCst); + thread_running.store(false, Ordering::SeqCst); + info!("AniMe system thread exited"); + }) + .map(|err| info!("AniMe system thread: {:?}", err)) + .ok(); + } + + fn write_bytes(&self, message: &[u8]) { + match self.handle.write_control( + 0x21, // request_type + 0x09, // request + 0x35e, // value + 0x00, // index + message, + Duration::from_millis(200), + ) { + Ok(_) => {} + Err(err) => match err { + rusb::Error::Timeout => {} + _ => error!("Failed to write to led interrupt: {}", err), + }, + } + } + + /// Write only a data packet. This will modify the leds brightness using the + /// global brightness set in config. + fn write_data_buffer(&self, mut buffer: AnimeDataBuffer) { + for led in buffer.get_mut()[7..].iter_mut() { + let mut bright = *led as f32 * self.config.brightness; + if bright > 254.0 { + bright = 254.0; + } + *led = bright as u8; + } + let data = AnimePacketType::from(buffer); + for row in data.iter() { + self.write_bytes(row); + } + self.write_bytes(&pkt_for_flush()); + } + + fn do_initialization(&self) { + let pkts = pkts_for_init(); + self.write_bytes(&pkts[0]); + self.write_bytes(&pkts[1]); + } +} + +pub struct CtrlAnimeTask<'a> { + inner: Arc>, + _c: Connection, + manager: ManagerProxy<'a>, +} + +impl<'a> CtrlAnimeTask<'a> { + pub fn new(inner: Arc>) -> Self { + let connection = Connection::new_system().unwrap(); + + let manager = ManagerProxy::new(&connection).unwrap(); + + let c1 = inner.clone(); + // Run this action when the system starts shutting down + manager + .connect_prepare_for_shutdown(move |shutdown| { + if shutdown { + 'outer: loop { + if let Ok(lock) = c1.try_lock() { + lock.thread_exit.store(true, Ordering::SeqCst); + CtrlAnime::run_thread(c1.clone(), lock.cache.shutdown.clone(), false); + break 'outer; + } + } + } + Ok(()) + }) + .map_err(|err| { + warn!("CtrlAnimeTask: new() {}", err); + err + }) + .ok(); + + let c1 = inner.clone(); + // Run this action when the system wakes up from sleep + manager + .connect_prepare_for_sleep(move |sleep| { + if !sleep { + // wait a fraction for things to wake up properly + std::thread::sleep(Duration::from_millis(100)); + 'outer: loop { + if let Ok(lock) = c1.try_lock() { + lock.thread_exit.store(true, Ordering::SeqCst); + CtrlAnime::run_thread(c1.clone(), lock.cache.wake.clone(), true); + break 'outer; + } + } + } + Ok(()) + }) + .map_err(|err| { + warn!("CtrlAnimeTask: new() {}", err); + err + }) + .ok(); + + Self { + inner, + _c: connection, + manager, + } + } +} + +impl<'a> crate::CtrlTask for CtrlAnimeTask<'a> { + fn do_task(&self) -> Result<(), RogError> { + if let Ok(mut lock) = self.inner.try_lock() { + // Refresh the config and cache incase the user has edited it + let config = AnimeConfig::load(); + lock.cache + .init_from_config(&config) + .map_err(|err| { + warn!("CtrlAnimeTask: do_task {}", err); + err + }) + .ok(); + } + + // Check for signals on each task iteration, this will run the callbacks + // if any signal is recieved + self.manager.next_signal()?; + Ok(()) + } +} + +pub struct CtrlAnimeReloader(pub Arc>); + +impl crate::Reloadable for CtrlAnimeReloader { + fn reload(&mut self) -> Result<(), RogError> { + if let Ok(lock) = self.0.try_lock() { + lock.write_bytes(&pkt_for_set_on(lock.config.awake_enabled)); + lock.write_bytes(&pkt_for_apply()); + lock.write_bytes(&pkt_for_set_boot(lock.config.boot_anim_enabled)); + lock.write_bytes(&pkt_for_apply()); + + let action = lock.cache.boot.clone(); + CtrlAnime::run_thread(self.0.clone(), action, true); + } + Ok(()) + } +} diff --git a/daemon/src/ctrl_anime/zbus.rs b/daemon/src/ctrl_anime/zbus.rs new file mode 100644 index 00000000..f5e28758 --- /dev/null +++ b/daemon/src/ctrl_anime/zbus.rs @@ -0,0 +1,140 @@ +use std::sync::{Arc, Mutex}; + +use log::warn; +use rog_anime::{ + usb::{pkt_for_apply, pkt_for_set_boot, pkt_for_set_on}, + AnimeDataBuffer, AnimePowerStates, +}; +use zbus::dbus_interface; +use zvariant::ObjectPath; + +use std::sync::atomic::Ordering; + +use super::CtrlAnime; + +pub struct CtrlAnimeZbus(pub Arc>); + +/// The struct with the main dbus methods requires this trait +impl crate::ZbusAdd for CtrlAnimeZbus { + fn add_to_server(self, server: &mut zbus::ObjectServer) { + server + .at( + &ObjectPath::from_str_unchecked("/org/asuslinux/Anime"), + self, + ) + .map_err(|err| { + warn!("CtrlAnimeDisplay: add_to_server {}", err); + err + }) + .ok(); + } +} + +// None of these calls can be guarnateed to succeed unless we loop until okay +// If the try_lock *does* succeed then any other thread trying to lock will not grab it +// until we finish. +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlAnimeZbus { + /// Writes a data stream of length. Will force system thread to exit until it is restarted + fn write(&self, input: AnimeDataBuffer) { + 'outer: loop { + if let Ok(lock) = self.0.try_lock() { + lock.thread_exit.store(true, Ordering::SeqCst); + lock.write_data_buffer(input); + break 'outer; + } + } + } + + /// Set the global AniMe brightness + fn set_brightness(&self, bright: f32) { + 'outer: loop { + if let Ok(mut lock) = self.0.try_lock() { + let mut bright = bright; + if bright < 0.0 { + bright = 0.0 + } else if bright > 254.0 { + bright = 254.0; + } + lock.config.brightness = bright; + lock.config.write(); + break 'outer; + } + } + } + + /// Set whether the AniMe is displaying images/data + fn set_on_off(&self, status: bool) { + 'outer: loop { + if let Ok(mut lock) = self.0.try_lock() { + lock.write_bytes(&pkt_for_set_on(status)); + lock.config.awake_enabled = status; + lock.config.write(); + + let states = AnimePowerStates { + enabled: lock.config.awake_enabled, + boot_anim_enabled: lock.config.boot_anim_enabled, + }; + self.notify_power_states(&states) + .unwrap_or_else(|err| warn!("{}", err)); + break 'outer; + } + } + } + + /// Set whether the AniMe will show boot, suspend, or off animations + fn set_boot_on_off(&self, on: bool) { + 'outer: loop { + if let Ok(mut lock) = self.0.try_lock() { + lock.write_bytes(&pkt_for_set_boot(on)); + lock.write_bytes(&pkt_for_apply()); + lock.config.boot_anim_enabled = on; + lock.config.write(); + + let states = AnimePowerStates { + enabled: lock.config.awake_enabled, + boot_anim_enabled: lock.config.boot_anim_enabled, + }; + self.notify_power_states(&states) + .unwrap_or_else(|err| warn!("{}", err)); + break 'outer; + } + } + } + + /// The main loop is the base system set action if the user isn't running + /// the user daemon + fn run_main_loop(&self, start: bool) { + if start { + 'outer: loop { + if let Ok(lock) = self.0.try_lock() { + lock.thread_exit.store(true, Ordering::SeqCst); + CtrlAnime::run_thread(self.0.clone(), lock.cache.system.clone(), false); + break 'outer; + } + } + } + } + + /// Get status of if the AniMe LEDs are on + #[dbus_interface(property)] + fn awake_enabled(&self) -> bool { + if let Ok(ctrl) = self.0.try_lock() { + return ctrl.config.awake_enabled; + } + true + } + + /// Get the status of if factory system-status animations are enabled + #[dbus_interface(property)] + fn boot_enabled(&self) -> bool { + if let Ok(ctrl) = self.0.try_lock() { + return ctrl.config.boot_anim_enabled; + } + true + } + + /// Notify listeners of the status of AniMe LED power and factory system-status animations + #[dbus_interface(signal)] + fn notify_power_states(&self, data: &AnimePowerStates) -> zbus::Result<()>; +} diff --git a/daemon/src/ctrl_aura/config.rs b/daemon/src/ctrl_aura/config.rs new file mode 100644 index 00000000..6e5d3778 --- /dev/null +++ b/daemon/src/ctrl_aura/config.rs @@ -0,0 +1,267 @@ +use crate::laptops::LaptopLedData; +use log::{error, info, warn}; +use rog_aura::{AuraEffect, AuraModeNum, AuraZone, LedBrightness}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; + +pub static AURA_CONFIG_PATH: &str = "/etc/asusd/aura.conf"; + +#[derive(Deserialize, Serialize)] +pub struct AuraConfigV320 { + pub brightness: u32, + pub current_mode: AuraModeNum, + pub builtins: BTreeMap, + pub multizone: Option, +} + +impl AuraConfigV320 { + pub(crate) fn into_current(self) -> AuraConfig { + AuraConfig { + brightness: ::from(self.brightness), + current_mode: self.current_mode, + builtins: self.builtins, + multizone: self.multizone, + awake_enabled: true, + sleep_anim_enabled: true, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AuraConfigV352 { + pub brightness: LedBrightness, + pub current_mode: AuraModeNum, + pub builtins: BTreeMap, + pub multizone: Option, +} + +impl AuraConfigV352 { + pub(crate) fn into_current(self) -> AuraConfig { + AuraConfig { + brightness: self.brightness, + current_mode: self.current_mode, + builtins: self.builtins, + multizone: self.multizone, + awake_enabled: true, + sleep_anim_enabled: true, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct AuraConfig { + pub brightness: LedBrightness, + pub current_mode: AuraModeNum, + pub builtins: BTreeMap, + pub multizone: Option, + pub awake_enabled: bool, + pub sleep_anim_enabled: bool, +} + +impl Default for AuraConfig { + fn default() -> Self { + AuraConfig { + brightness: LedBrightness::Med, + current_mode: AuraModeNum::Static, + builtins: BTreeMap::new(), + multizone: None, + awake_enabled: true, + sleep_anim_enabled: true, + } + } +} + +impl AuraConfig { + /// `load` will attempt to read the config, and panic if the dir is missing + pub fn load(supported_led_modes: &LaptopLedData) -> Self { + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&AURA_CONFIG_PATH) + .unwrap_or_else(|_| { + panic!( + "The file {} or directory /etc/asusd/ is missing", + AURA_CONFIG_PATH + ) + }); // okay to cause panic here + let mut buf = String::new(); + if let Ok(read_len) = file.read_to_string(&mut buf) { + if read_len == 0 { + return AuraConfig::create_default(&mut file, supported_led_modes); + } else { + if let Ok(data) = serde_json::from_str(&buf) { + return data; + } else if let Ok(data) = serde_json::from_str::(&buf) { + let config = data.into_current(); + config.write(); + info!("Updated AuraConfig version"); + return config; + } else if let Ok(data) = serde_json::from_str::(&buf) { + let config = data.into_current(); + config.write(); + info!("Updated AuraConfig version"); + return config; + } + warn!("Could not deserialise {}", AURA_CONFIG_PATH); + panic!("Please remove {} then restart asusd", AURA_CONFIG_PATH); + } + } + AuraConfig::create_default(&mut file, supported_led_modes) + } + + fn create_default(file: &mut File, support_data: &LaptopLedData) -> Self { + // create a default config here + let mut config = AuraConfig::default(); + + for n in &support_data.standard { + config + .builtins + .insert(*n, AuraEffect::default_with_mode(*n)); + } + + // Should be okay to unwrap this as is since it is a Default + let json = serde_json::to_string(&config).unwrap(); + file.write_all(json.as_bytes()) + .unwrap_or_else(|_| panic!("Could not write {}", AURA_CONFIG_PATH)); + config + } + + pub fn read(&mut self) { + let mut file = OpenOptions::new() + .read(true) + .open(&AURA_CONFIG_PATH) + .unwrap_or_else(|err| panic!("Error reading {}: {}", AURA_CONFIG_PATH, err)); + let mut buf = String::new(); + if let Ok(l) = file.read_to_string(&mut buf) { + if l == 0 { + warn!("File is empty {}", AURA_CONFIG_PATH); + } else { + let x: AuraConfig = serde_json::from_str(&buf) + .unwrap_or_else(|_| panic!("Could not deserialise {}", AURA_CONFIG_PATH)); + *self = x; + } + } + } + + pub fn write(&self) { + let mut file = File::create(AURA_CONFIG_PATH).expect("Couldn't overwrite config"); + let json = serde_json::to_string_pretty(self).expect("Parse config to JSON failed"); + file.write_all(json.as_bytes()) + .unwrap_or_else(|err| error!("Could not write config: {}", err)); + } + + /// Multipurpose, will accept AuraEffect with zones and put in the correct store + pub fn set_builtin(&mut self, effect: AuraEffect) { + match effect.zone() { + AuraZone::None => { + self.builtins.insert(*effect.mode(), effect); + } + _ => { + if let Some(multi) = self.multizone.as_mut() { + multi.set(effect) + } + } + } + } + + pub fn get_multizone(&self, aura_type: AuraModeNum) -> Option<&[AuraEffect; 4]> { + if let Some(multi) = &self.multizone { + if aura_type == AuraModeNum::Static { + return Some(multi.static_()); + } else if aura_type == AuraModeNum::Breathe { + return Some(multi.breathe()); + } + } + None + } +} + +#[derive(Deserialize, Serialize)] +pub struct AuraMultiZone { + static_: [AuraEffect; 4], + breathe: [AuraEffect; 4], +} + +impl AuraMultiZone { + pub fn set(&mut self, effect: AuraEffect) { + if effect.mode == AuraModeNum::Static { + match effect.zone { + AuraZone::None => {} + AuraZone::One => self.static_[0] = effect, + AuraZone::Two => self.static_[1] = effect, + AuraZone::Three => self.static_[2] = effect, + AuraZone::Four => self.static_[3] = effect, + } + } else if effect.mode == AuraModeNum::Breathe { + match effect.zone { + AuraZone::None => {} + AuraZone::One => self.breathe[0] = effect, + AuraZone::Two => self.breathe[1] = effect, + AuraZone::Three => self.breathe[2] = effect, + AuraZone::Four => self.breathe[3] = effect, + } + } + } + + pub fn static_(&self) -> &[AuraEffect; 4] { + &self.static_ + } + + pub fn breathe(&self) -> &[AuraEffect; 4] { + &self.breathe + } +} + +impl Default for AuraMultiZone { + fn default() -> Self { + Self { + static_: [ + AuraEffect { + mode: AuraModeNum::Static, + zone: AuraZone::One, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Static, + zone: AuraZone::Two, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Static, + zone: AuraZone::Three, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Static, + zone: AuraZone::Four, + ..Default::default() + }, + ], + breathe: [ + AuraEffect { + mode: AuraModeNum::Breathe, + zone: AuraZone::One, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Breathe, + zone: AuraZone::Two, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Breathe, + zone: AuraZone::Three, + ..Default::default() + }, + AuraEffect { + mode: AuraModeNum::Breathe, + zone: AuraZone::Four, + ..Default::default() + }, + ], + } + } +} diff --git a/daemon/src/ctrl_aura/controller.rs b/daemon/src/ctrl_aura/controller.rs new file mode 100644 index 00000000..5cef3651 --- /dev/null +++ b/daemon/src/ctrl_aura/controller.rs @@ -0,0 +1,395 @@ +// Only these two packets must be 17 bytes +static KBD_BRIGHT_PATH: &str = "/sys/class/leds/asus::kbd_backlight/brightness"; + +use crate::{ + error::RogError, + laptops::{LaptopLedData, ASUS_KEYBOARD_DEVICES}, + CtrlTask, +}; +use log::{info, warn}; +use logind_zbus::ManagerProxy; +use rog_aura::{ + usb::{ + LED_APPLY, LED_AWAKE_OFF_SLEEP_OFF, LED_AWAKE_OFF_SLEEP_ON, LED_AWAKE_ON_SLEEP_OFF, + LED_AWAKE_ON_SLEEP_ON, LED_SET, + }, + AuraEffect, LedBrightness, LED_MSG_LEN, +}; +use rog_types::supported::LedSupportedFunctions; +use std::io::{Read, Write}; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; +use std::{fs::OpenOptions, thread::spawn}; +use zbus::Connection; + +use crate::GetSupported; + +use super::config::AuraConfig; + +impl GetSupported for CtrlKbdLed { + type A = LedSupportedFunctions; + + fn get_supported() -> Self::A { + // let mode = <&str>::from(&::from(*mode)); + let multizone_led_mode = false; + let per_key_led_mode = false; + let laptop = LaptopLedData::get_data(); + let stock_led_modes = laptop.standard; + + LedSupportedFunctions { + brightness_set: CtrlKbdLed::get_kbd_bright_path().is_some(), + stock_led_modes, + multizone_led_mode, + per_key_led_mode, + } + } +} + +pub struct CtrlKbdLed { + pub led_node: Option, + pub bright_node: String, + pub supported_modes: LaptopLedData, + pub flip_effect_write: bool, + pub config: AuraConfig, +} + +pub struct CtrlKbdLedTask<'a> { + inner: Arc>, + _c: Connection, + manager: ManagerProxy<'a>, +} + +impl<'a> CtrlKbdLedTask<'a> { + pub fn new(inner: Arc>) -> Self { + let connection = Connection::new_system().unwrap(); + + let manager = ManagerProxy::new(&connection).unwrap(); + + let c1 = inner.clone(); + // Run this action when the system wakes up from sleep + manager + .connect_prepare_for_sleep(move |sleep| { + if !sleep { + let c1 = c1.clone(); + spawn(move || { + // wait a fraction for things to wake up properly + //std::thread::sleep(Duration::from_millis(100)); + loop { + if let Ok(ref mut lock) = c1.try_lock() { + lock.set_brightness(lock.config.brightness).ok(); + break; + } + } + }); + } + Ok(()) + }) + .map_err(|err| { + warn!("CtrlAnimeTask: new() {}", err); + err + }) + .ok(); + + Self { + inner, + _c: connection, + manager, + } + } + + fn update_config(lock: &mut CtrlKbdLed) -> Result<(), RogError> { + let mut file = OpenOptions::new() + .read(true) + .open(&lock.bright_node) + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => { + RogError::MissingLedBrightNode((&lock.bright_node).into(), err) + } + _ => RogError::Path((&lock.bright_node).into(), err), + })?; + let mut buf = [0u8; 1]; + file.read_exact(&mut buf) + .map_err(|err| RogError::Read("buffer".into(), err))?; + if let Some(num) = char::from(buf[0]).to_digit(10) { + if lock.config.brightness != num.into() { + lock.config.read(); + lock.config.brightness = num.into(); + lock.config.write(); + } + return Ok(()); + } + Err(RogError::ParseLed) + } +} + +impl<'a> CtrlTask for CtrlKbdLedTask<'a> { + fn do_task(&self) -> Result<(), RogError> { + self.manager.next_signal()?; + if let Ok(ref mut lock) = self.inner.try_lock() { + return Self::update_config(lock); + } + Ok(()) + } +} + +pub struct CtrlKbdLedReloader(pub Arc>); + +impl crate::Reloadable for CtrlKbdLedReloader { + fn reload(&mut self) -> Result<(), RogError> { + if let Ok(mut ctrl) = self.0.try_lock() { + let current = ctrl.config.current_mode; + if let Some(mode) = ctrl.config.builtins.get(¤t).cloned() { + ctrl.do_command(mode).ok(); + } + + ctrl.set_states_enabled(ctrl.config.awake_enabled, ctrl.config.sleep_anim_enabled) + .map_err(|err| warn!("{}", err)) + .ok(); + } + Ok(()) + } +} + +pub struct CtrlKbdLedZbus(pub Arc>); + +impl CtrlKbdLedZbus { + pub fn new(inner: Arc>) -> Self { + Self(inner) + } +} + +impl CtrlKbdLed { + #[inline] + pub fn new(supported_modes: LaptopLedData, config: AuraConfig) -> Result { + // TODO: return error if *all* nodes are None + let mut led_node = None; + for prod in ASUS_KEYBOARD_DEVICES.iter() { + match Self::find_led_node(prod) { + Ok(node) => { + led_node = Some(node); + break; + } + Err(err) => warn!("led_node: {}", err), + } + } + + let bright_node = Self::get_kbd_bright_path(); + + if led_node.is_none() && bright_node.is_none() { + return Err(RogError::MissingFunction( + "All keyboard features missing, you may require a v5.11 series kernel or newer" + .into(), + )); + } + + if bright_node.is_none() { + return Err(RogError::MissingFunction( + "No brightness control, you may require a v5.11 series kernel or newer".into(), + )); + } + + let ctrl = CtrlKbdLed { + led_node, + bright_node: bright_node.unwrap(), // If was none then we already returned above + supported_modes, + flip_effect_write: false, + config, + }; + Ok(ctrl) + } + + fn get_kbd_bright_path() -> Option { + if Path::new(KBD_BRIGHT_PATH).exists() { + return Some(KBD_BRIGHT_PATH.to_string()); + } + None + } + + pub(super) fn get_brightness(&self) -> Result { + let mut file = OpenOptions::new() + .read(true) + .open(&self.bright_node) + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => { + RogError::MissingLedBrightNode((&self.bright_node).into(), err) + } + _ => RogError::Path((&self.bright_node).into(), err), + })?; + let mut buf = [0u8; 1]; + file.read_exact(&mut buf) + .map_err(|err| RogError::Read("buffer".into(), err))?; + Ok(buf[0]) + } + + pub(super) fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + let path = Path::new(&self.bright_node); + let mut file = + OpenOptions::new() + .write(true) + .open(&path) + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => { + RogError::MissingLedBrightNode((&self.bright_node).into(), err) + } + _ => RogError::Path((&self.bright_node).into(), err), + })?; + file.write_all(&[brightness.as_char_code()]) + .map_err(|err| RogError::Read("buffer".into(), err))?; + Ok(()) + } + + /// Set if awake/on LED active, and/or sleep animation active + pub(super) fn set_states_enabled(&self, awake: bool, sleep: bool) -> Result<(), RogError> { + let bytes = if awake && sleep { + LED_AWAKE_ON_SLEEP_ON + } else if awake && !sleep { + LED_AWAKE_ON_SLEEP_OFF + } else if !awake && sleep { + LED_AWAKE_OFF_SLEEP_ON + } else if !awake && !sleep { + LED_AWAKE_OFF_SLEEP_OFF + } else { + LED_AWAKE_ON_SLEEP_ON + }; + self.write_bytes(&bytes)?; + self.write_bytes(&LED_SET)?; + // Changes won't persist unless apply is set + self.write_bytes(&LED_APPLY)?; + Ok(()) + } + + fn find_led_node(id_product: &str) -> Result { + let mut enumerator = udev::Enumerator::new().map_err(|err| { + warn!("{}", err); + RogError::Udev("enumerator failed".into(), err) + })?; + enumerator.match_subsystem("hidraw").map_err(|err| { + warn!("{}", err); + RogError::Udev("match_subsystem failed".into(), err) + })?; + + for device in enumerator.scan_devices().map_err(|err| { + warn!("{}", err); + RogError::Udev("scan_devices failed".into(), err) + })? { + if let Some(parent) = device + .parent_with_subsystem_devtype("usb", "usb_device") + .map_err(|err| { + warn!("{}", err); + RogError::Udev("parent_with_subsystem_devtype failed".into(), err) + })? + { + if parent + .attribute_value("idProduct") + .ok_or_else(|| RogError::NotFound("LED idProduct".into()))? + == id_product + { + if let Some(dev_node) = device.devnode() { + info!("Using device at: {:?} for LED control", dev_node); + return Ok(dev_node.to_string_lossy().to_string()); + } + } + } + } + Err(RogError::MissingFunction( + "ASUS LED device node not found".into(), + )) + } + + pub(crate) fn do_command(&mut self, mode: AuraEffect) -> Result<(), RogError> { + self.set_and_save(mode) + } + + /// Should only be used if the bytes you are writing are verified correct + #[inline] + fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { + if let Some(led_node) = &self.led_node { + if let Ok(mut file) = OpenOptions::new().write(true).open(led_node) { + // println!("write: {:02x?}", &message); + return file + .write_all(message) + .map_err(|err| RogError::Write("write_bytes".into(), err)); + } + } + Err(RogError::NotSupported) + } + + /// Write an effect block + #[inline] + fn _write_effect(&mut self, effect: &[Vec]) -> Result<(), RogError> { + if self.flip_effect_write { + for row in effect.iter().rev() { + self.write_bytes(row)?; + } + } else { + for row in effect.iter() { + self.write_bytes(row)?; + } + } + self.flip_effect_write = !self.flip_effect_write; + Ok(()) + } + + /// Used to set a builtin mode and save the settings for it + /// + /// This needs to be universal so that settings applied by dbus stick + #[inline] + fn set_and_save(&mut self, mode: AuraEffect) -> Result<(), RogError> { + self.config.read(); + self.write_mode(&mode)?; + self.config.current_mode = *mode.mode(); + self.config.set_builtin(mode); + self.config.write(); + Ok(()) + } + + #[inline] + pub(super) fn toggle_mode(&mut self, reverse: bool) -> Result<(), RogError> { + let current = self.config.current_mode; + if let Some(idx) = self + .supported_modes + .standard + .iter() + .position(|v| *v == current) + { + let mut idx = idx; + // goes past end of array + if reverse { + if idx == 0 { + idx = self.supported_modes.standard.len() - 1; + } else { + idx -= 1; + } + } else { + idx += 1; + if idx == self.supported_modes.standard.len() { + idx = 0; + } + } + let next = self.supported_modes.standard[idx]; + + self.config.read(); + if let Some(data) = self.config.builtins.get(&next) { + self.write_mode(data)?; + self.config.current_mode = next; + } + self.config.write(); + } + + Ok(()) + } + + #[inline] + fn write_mode(&self, mode: &AuraEffect) -> Result<(), RogError> { + if !self.supported_modes.standard.contains(mode.mode()) { + return Err(RogError::NotSupported); + } + let bytes: [u8; LED_MSG_LEN] = mode.into(); + self.write_bytes(&bytes)?; + self.write_bytes(&LED_SET)?; + // Changes won't persist unless apply is set + self.write_bytes(&LED_APPLY)?; + Ok(()) + } +} diff --git a/daemon/src/ctrl_aura/mod.rs b/daemon/src/ctrl_aura/mod.rs new file mode 100644 index 00000000..85a4d229 --- /dev/null +++ b/daemon/src/ctrl_aura/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod controller; +pub mod zbus; diff --git a/daemon/src/ctrl_aura/zbus.rs b/daemon/src/ctrl_aura/zbus.rs new file mode 100644 index 00000000..d96fa095 --- /dev/null +++ b/daemon/src/ctrl_aura/zbus.rs @@ -0,0 +1,165 @@ +use log::{error, warn}; +use rog_aura::{AuraEffect, LedBrightness, LedPowerStates}; +use zbus::dbus_interface; +use zvariant::ObjectPath; + +use super::controller::CtrlKbdLedZbus; + +impl crate::ZbusAdd for CtrlKbdLedZbus { + fn add_to_server(self, server: &mut zbus::ObjectServer) { + server + .at(&ObjectPath::from_str_unchecked("/org/asuslinux/Led"), self) + .map_err(|err| { + error!("DbusKbdLed: add_to_server {}", err); + }) + .ok(); + } +} + +/// The main interface for changing, reading, or notfying signals +/// +/// LED commands are split between Brightness, Modes, Per-Key +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlKbdLedZbus { + /// Set the keyboard brightness level (0-3) + fn set_brightness(&mut self, brightness: LedBrightness) { + if let Ok(ctrl) = self.0.try_lock() { + ctrl.set_brightness(brightness) + .map_err(|err| warn!("{}", err)) + .ok(); + } + } + + /// Set the keyboard LED to enabled while the device is awake + fn set_awake_enabled(&mut self, enabled: bool) { + if let Ok(mut ctrl) = self.0.try_lock() { + ctrl.set_states_enabled(enabled, ctrl.config.sleep_anim_enabled) + .map_err(|err| warn!("{}", err)) + .ok(); + ctrl.config.awake_enabled = enabled; + ctrl.config.write(); + + let states = LedPowerStates { + enabled: ctrl.config.awake_enabled, + sleep_anim_enabled: ctrl.config.sleep_anim_enabled, + }; + self.notify_power_states(&states) + .unwrap_or_else(|err| warn!("{}", err)); + } + } + + /// Set the keyboard LED suspend animation to enabled while the device is suspended + fn set_sleep_enabled(&mut self, enabled: bool) { + if let Ok(mut ctrl) = self.0.try_lock() { + ctrl.set_states_enabled(ctrl.config.awake_enabled, enabled) + .map_err(|err| warn!("{}", err)) + .ok(); + ctrl.config.sleep_anim_enabled = enabled; + ctrl.config.write(); + let states = LedPowerStates { + enabled: ctrl.config.awake_enabled, + sleep_anim_enabled: ctrl.config.sleep_anim_enabled, + }; + self.notify_power_states(&states) + .unwrap_or_else(|err| warn!("{}", err)); + } + } + + fn set_led_mode(&mut self, effect: AuraEffect) { + if let Ok(mut ctrl) = self.0.try_lock() { + match ctrl.do_command(effect) { + Ok(_) => { + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + self.notify_led(mode.clone()) + .unwrap_or_else(|err| warn!("{}", err)); + } + } + Err(err) => { + warn!("{}", err); + } + } + } + } + + fn next_led_mode(&self) { + if let Ok(mut ctrl) = self.0.try_lock() { + ctrl.toggle_mode(false) + .unwrap_or_else(|err| warn!("{}", err)); + + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + self.notify_led(mode.clone()) + .unwrap_or_else(|err| warn!("{}", err)); + } + } + } + + fn prev_led_mode(&self) { + if let Ok(mut ctrl) = self.0.try_lock() { + ctrl.toggle_mode(true) + .unwrap_or_else(|err| warn!("{}", err)); + + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + self.notify_led(mode.clone()) + .unwrap_or_else(|err| warn!("{}", err)); + } + } + } + + #[dbus_interface(property)] + fn awake_enabled(&self) -> bool { + if let Ok(ctrl) = self.0.try_lock() { + return ctrl.config.awake_enabled; + } + true + } + + #[dbus_interface(property)] + fn sleep_enabled(&self) -> bool { + if let Ok(ctrl) = self.0.try_lock() { + return ctrl.config.sleep_anim_enabled; + } + true + } + + /// Return the current mode data + #[dbus_interface(property)] + fn led_mode(&self) -> String { + if let Ok(ctrl) = self.0.try_lock() { + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + if let Ok(json) = serde_json::to_string(&mode) { + return json; + } + } + } + warn!("SetKeyBacklight could not deserialise"); + "SetKeyBacklight could not deserialise".to_string() + } + + /// Return a list of available modes + #[dbus_interface(property)] + fn led_modes(&self) -> String { + if let Ok(ctrl) = self.0.try_lock() { + if let Ok(json) = serde_json::to_string(&ctrl.config.builtins) { + return json; + } + } + warn!("SetKeyBacklight could not deserialise"); + "SetKeyBacklight could not serialise".to_string() + } + + /// Return the current LED brightness + #[dbus_interface(property)] + fn led_brightness(&self) -> i8 { + if let Ok(ctrl) = self.0.try_lock() { + return ctrl.get_brightness().map(|n| n as i8).unwrap_or(-1); + } + warn!("SetKeyBacklight could not serialise"); + -1 + } + + #[dbus_interface(signal)] + fn notify_led(&self, data: AuraEffect) -> zbus::Result<()>; + + #[dbus_interface(signal)] + fn notify_power_states(&self, data: &LedPowerStates) -> zbus::Result<()>; +} diff --git a/daemon/src/ctrl_profiles/config.rs b/daemon/src/ctrl_profiles/config.rs new file mode 100644 index 00000000..9907ba5f --- /dev/null +++ b/daemon/src/ctrl_profiles/config.rs @@ -0,0 +1,92 @@ +use log::{error, warn}; +use rog_profiles::error::ProfileError; +use rog_profiles::{FanCurves, Profile}; +use serde_derive::{Deserialize, Serialize}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct ProfileConfig { + #[serde(skip)] + config_path: String, + /// For restore on boot + pub active: Profile, + /// States to restore + pub fan_curves: Option, +} + +impl ProfileConfig { + fn new(config_path: String) -> Result { + let mut platform = ProfileConfig { + config_path, + active: Profile::Balanced, + fan_curves: None, + }; + + if !Profile::is_platform_profile_supported() { + return Err(ProfileError::NotSupported); + } + + if FanCurves::is_fan_curves_supported() { + let mut curves = FanCurves::default(); + curves.update_from_platform(); + platform.fan_curves = Some(curves); + } + Ok(platform) + } +} + +impl ProfileConfig { + pub fn load(config_path: String) -> Self { + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&config_path) + .unwrap_or_else(|_| panic!("The directory /etc/asusd/ is missing")); // okay to cause panic here + let mut buf = String::new(); + let mut config; + if let Ok(read_len) = file.read_to_string(&mut buf) { + if read_len == 0 { + config = Self::new(config_path).unwrap(); + } else if let Ok(data) = serde_json::from_str(&buf) { + config = data; + config.config_path = config_path; + } else { + warn!("Could not deserialise {}", config_path); + panic!("Please remove {} then restart service", config_path); + } + } else { + config = Self::new(config_path).unwrap() + } + config.write(); + config + } + + pub fn read(&mut self) { + let mut file = OpenOptions::new() + .read(true) + .open(&self.config_path) + .unwrap_or_else(|err| panic!("Error reading {}: {}", self.config_path, err)); + + let mut buf = String::new(); + if let Ok(l) = file.read_to_string(&mut buf) { + if l == 0 { + warn!("File is empty {}", self.config_path); + } else { + let mut data: ProfileConfig = serde_json::from_str(&buf) + .unwrap_or_else(|_| panic!("Could not deserialise {}", self.config_path)); + // copy over serde skipped values + data.config_path = self.config_path.clone(); + *self = data; + } + } + } + + pub fn write(&self) { + let mut file = File::create(&self.config_path).expect("Couldn't overwrite config"); + let json = serde_json::to_string_pretty(self).expect("Parse config to JSON failed"); + file.write_all(json.as_bytes()) + .unwrap_or_else(|err| error!("Could not write config: {}", err)); + } +} diff --git a/daemon/src/daemon.rs b/daemon/src/daemon.rs index d04d285f..808df1a0 100644 --- a/daemon/src/daemon.rs +++ b/daemon/src/daemon.rs @@ -10,18 +10,20 @@ use daemon::ctrl_profiles::controller::CtrlPlatformTask; use daemon::{ config::Config, ctrl_supported::SupportedFunctions, laptops::print_board_info, GetSupported, }; -use daemon::{ctrl_anime::*, ctrl_gfx::controller::CtrlGraphics}; +use daemon::{ctrl_anime::*}; use daemon::{ ctrl_profiles::{controller::CtrlPlatformProfile, zbus::ProfileZbus}, laptops::LaptopLedData, }; +use supergfxctl::config::GfxConfig; +use supergfxctl::controller::CtrlGraphics; +use supergfxctl::gfx_vendors::GfxVendors; use ::zbus::{fdo, Connection, ObjectServer}; use daemon::{CtrlTask, Reloadable, ZbusAdd}; use log::LevelFilter; use log::{error, info, warn}; use rog_dbus::DBUS_NAME; -use rog_types::gfx_vendors::GfxVendors; use std::env; use std::error::Error; use std::io::Write; @@ -32,6 +34,7 @@ use daemon::ctrl_rog_bios::CtrlRogBios; use zvariant::ObjectPath; static PROFILE_CONFIG_PATH: &str = "/etc/asusd/profile.conf"; +static GFX_CONFIG_PATH: &str = "/etc/asusd/supergfx.conf"; pub fn main() -> Result<(), Box> { let mut logger = env_logger::Builder::new(); @@ -81,9 +84,12 @@ fn start_daemon() -> Result<(), Box> { let mut object_server = ObjectServer::new(&connection); let config = Config::load(); - let enable_gfx_switching = config.gfx_managed; let config = Arc::new(Mutex::new(config)); + let gfx_config = GfxConfig::load(GFX_CONFIG_PATH.into()); + let enable_gfx_switching = gfx_config.gfx_managed; + let gfx_config = Arc::new(Mutex::new(gfx_config)); + supported.add_to_server(&mut object_server); match CtrlRogBios::new(config.clone()) { @@ -169,12 +175,12 @@ fn start_daemon() -> Result<(), Box> { // Graphics switching requires some checks on boot specifically for g-sync capable laptops if enable_gfx_switching { - match CtrlGraphics::new(config.clone()) { + match CtrlGraphics::new(gfx_config.clone()) { Ok(mut ctrl) => { // Need to check if a laptop has the dedicated gfx switch if CtrlRogBios::has_dedicated_gfx_toggle() { if let Ok(ded) = CtrlRogBios::get_gfx_mode() { - if let Ok(config) = config.lock() { + if let Ok(config) = gfx_config.lock() { if ded == 1 { warn!("Dedicated GFX toggle is on but driver mode is not nvidia \nSetting to nvidia driver mode"); let devices = ctrl.devices(); diff --git a/daemon/src/error.rs b/daemon/src/error.rs index c3052c8c..27384ef0 100644 --- a/daemon/src/error.rs +++ b/daemon/src/error.rs @@ -1,10 +1,9 @@ use rog_profiles::error::ProfileError; use rog_types::error::GraphicsError; +use supergfxctl::error::GfxError; use std::convert::From; use std::fmt; -use crate::ctrl_gfx::error::GfxError; - #[derive(Debug)] pub enum RogError { ParseVendor, @@ -24,7 +23,6 @@ pub enum RogError { Profiles(ProfileError), Initramfs(String), Modprobe(String), - Command(String, std::io::Error), Io(std::io::Error), Zbus(zbus::Error), } @@ -50,7 +48,6 @@ impl fmt::Display for RogError { RogError::Profiles(deets) => write!(f, "Profile error: {}", deets), RogError::Initramfs(detail) => write!(f, "Initiramfs error: {}", detail), RogError::Modprobe(detail) => write!(f, "Modprobe error: {}", detail), - RogError::Command(func, error) => write!(f, "Command exec error: {}: {}", func, error), RogError::Io(detail) => write!(f, "std::io error: {}", detail), RogError::Zbus(detail) => write!(f, "Zbus error: {}", detail), } diff --git a/daemon/src/lib.rs b/daemon/src/lib.rs index f87a5cf4..82984d50 100644 --- a/daemon/src/lib.rs +++ b/daemon/src/lib.rs @@ -8,8 +8,6 @@ pub mod ctrl_anime; pub mod ctrl_aura; /// Control of battery charge level pub mod ctrl_charge; -/// GPU switching and power -pub mod ctrl_gfx; /// Control CPU min/max freq and turbo, fan mode, fan curves /// /// Intel machines can control: diff --git a/rog-dbus/Cargo.toml b/rog-dbus/Cargo.toml index 4fe86180..aa97b47d 100644 --- a/rog-dbus/Cargo.toml +++ b/rog-dbus/Cargo.toml @@ -14,6 +14,7 @@ rog_anime = { path = "../rog-anime" } rog_aura = { path = "../rog-aura" } rog_profiles = { path = "../rog-profiles" } rog_types = { path = "../rog-types" } +supergfxctl = { path = "../supergfx" } zbus = "^1.9" zbus_macros = "^1.9" zvariant = "^2.8" diff --git a/rog-dbus/src/lib.rs b/rog-dbus/src/lib.rs index d7709969..7babbb09 100644 --- a/rog-dbus/src/lib.rs +++ b/rog-dbus/src/lib.rs @@ -13,7 +13,7 @@ pub mod zbus_supported; use rog_anime::AnimePowerStates; use rog_aura::{AuraEffect, LedPowerStates}; use rog_profiles::Profile; -use rog_types::gfx_vendors::{GfxRequiredUserAction, GfxVendors}; +use supergfxctl::gfx_vendors::{GfxRequiredUserAction, GfxVendors}; use std::sync::mpsc::{channel, Receiver}; use zbus::{Connection, Result, SignalReceiver}; diff --git a/rog-dbus/src/zbus_gfx.rs b/rog-dbus/src/zbus_gfx.rs index d60e6c02..a84f1e6f 100644 --- a/rog-dbus/src/zbus_gfx.rs +++ b/rog-dbus/src/zbus_gfx.rs @@ -21,7 +21,7 @@ use std::sync::mpsc::Sender; -use rog_types::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; +use supergfxctl::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; use zbus::{dbus_proxy, Connection, Result}; #[dbus_proxy( diff --git a/rog-types/src/lib.rs b/rog-types/src/lib.rs index 15078b86..478c9e5b 100644 --- a/rog-types/src/lib.rs +++ b/rog-types/src/lib.rs @@ -6,8 +6,6 @@ pub static DBUS_NAME: &str = "org.asuslinux.Daemon"; pub static DBUS_PATH: &str = "/org/asuslinux/Daemon"; pub static DBUS_IFACE: &str = "org.asuslinux.Daemon"; -pub mod gfx_vendors; - pub mod supported; pub mod error; diff --git a/supergfx/Cargo.toml b/supergfx/Cargo.toml new file mode 100644 index 00000000..c67e654f --- /dev/null +++ b/supergfx/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "supergfxctl" +version = "1.1.0" +license = "MPL-2.0" +readme = "README.md" +authors = ["Luke "] +repository = "https://gitlab.com/asus-linux/asusctl" +homepage = "https://gitlab.com/asus-linux/asusctl" +documentation = "https://docs.rs/rog-anime" +description = "Types useful for fancy keyboards on ASUS ROG laptops" +keywords = ["graphics", "nvidia", "switching"] +edition = "2018" +exclude = ["data"] + +[dependencies] +serde = "^1.0" +serde_derive = "^1.0" +serde_json = "^1.0" +log = "^0.4" + +zbus = "^1.9.1" +zvariant = "^2.8" +zvariant_derive = "^2.8" +logind-zbus = "^0.7.1" + +sysfs-class = "^0.1.2" \ No newline at end of file diff --git a/supergfx/LICENSE b/supergfx/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/supergfx/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/supergfx/src/config.rs b/supergfx/src/config.rs new file mode 100644 index 00000000..e05409d2 --- /dev/null +++ b/supergfx/src/config.rs @@ -0,0 +1,89 @@ +use log::{error, warn}; +use serde_derive::{Deserialize, Serialize}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; + +use crate::gfx_vendors::GfxVendors; + +pub static CONFIG_PATH: &str = "/etc/asusd/asusd.conf"; +pub static AURA_CONFIG_PATH: &str = "/etc/asusd/asusd.conf"; + +#[derive(Deserialize, Serialize)] +pub struct GfxConfig { + #[serde(skip)] + config_path: String, + /// The current mode set, also applies on boot + pub gfx_mode: GfxVendors, + /// Only for informational purposes + #[serde(skip)] + pub gfx_tmp_mode: Option, + /// Set if graphics management is enabled + pub gfx_managed: bool, + /// Set if vfio option is enabled. This requires the vfio drivers to be built as modules + pub gfx_vfio_enable: bool, +} + +impl GfxConfig { + fn new(config_path: String) -> Self { + Self { + config_path, + gfx_mode: GfxVendors::Hybrid, + gfx_tmp_mode: None, + gfx_managed: true, + gfx_vfio_enable: false, + } + } + + /// `load` will attempt to read the config, and panic if the dir is missing + pub fn load(config_path: String) -> Self { + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&config_path) + .unwrap_or_else(|_| panic!("The directory {} is missing", config_path)); // okay to cause panic here + let mut buf = String::new(); + let mut config; + if let Ok(read_len) = file.read_to_string(&mut buf) { + if read_len == 0 { + config = Self::new(config_path); + } else if let Ok(data) = serde_json::from_str(&buf) { + config = data; + config.config_path = config_path; + } else { + warn!("Could not deserialise {}", config_path); + panic!("Please remove {} then restart service", config_path); + } + } else { + config = Self::new(config_path) + } + config.write(); + config + } + + pub fn read(&mut self) { + let mut file = OpenOptions::new() + .read(true) + .open(&self.config_path) + .unwrap_or_else(|err| panic!("Error reading {}: {}", self.config_path, err)); + let mut buf = String::new(); + if let Ok(l) = file.read_to_string(&mut buf) { + if l == 0 { + warn!("File is empty {}", self.config_path); + } else { + let mut x: Self = serde_json::from_str(&buf) + .unwrap_or_else(|_| panic!("Could not deserialise {}", self.config_path)); + // copy over serde skipped values + x.gfx_tmp_mode = self.gfx_tmp_mode; + *self = x; + } + } + } + + pub fn write(&self) { + let mut file = File::create(&self.config_path).expect("Couldn't overwrite config"); + let json = serde_json::to_string_pretty(self).expect("Parse config to JSON failed"); + file.write_all(json.as_bytes()) + .unwrap_or_else(|err| error!("Could not write config: {}", err)); + } +} diff --git a/daemon/src/ctrl_gfx/controller.rs b/supergfx/src/controller.rs similarity index 90% rename from daemon/src/ctrl_gfx/controller.rs rename to supergfx/src/controller.rs index 9fa83233..9aeae7eb 100644 --- a/daemon/src/ctrl_gfx/controller.rs +++ b/supergfx/src/controller.rs @@ -1,24 +1,23 @@ -use ::zbus::Connection; -use ctrl_gfx::error::GfxError; -use ctrl_gfx::*; -use ctrl_rog_bios::CtrlRogBios; use log::{error, info, warn}; use logind_zbus::{ types::{SessionClass, SessionInfo, SessionState, SessionType}, ManagerProxy, SessionProxy, }; -use rog_types::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; +use ::zbus::Connection; use std::{io::Write, ops::Add, path::Path, time::Instant}; use std::{process::Command, thread::sleep, time::Duration}; use std::{str::FromStr, sync::mpsc}; use std::{sync::Arc, sync::Mutex}; use sysfs_class::RuntimePM; use sysfs_class::{PciDevice, SysClass}; -use system::{GraphicsDevice, PciBus}; -use crate::*; +use crate::{*, error::GfxError, system::{GraphicsDevice, PciBus}}; + +use super::config::GfxConfig; +use super::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; const THREAD_TIMEOUT_MSG: &str = "GFX: thread time exceeded 3 minutes, exiting"; +const NVIDIA_RUNTIME_STATUS_PATH: &str = "/sys/bus/pci/devices/0000:01:00.0/power/runtime_status"; pub struct CtrlGraphics { bus: PciBus, @@ -27,20 +26,12 @@ pub struct CtrlGraphics { nvidia: Vec, #[allow(dead_code)] other: Vec, - config: Arc>, + config: Arc>, thread_kill: Arc>>>, } -impl Reloadable for CtrlGraphics { - fn reload(&mut self) -> Result<(), RogError> { - self.auto_power()?; - info!("GFX: Reloaded gfx mode: {:?}", self.get_gfx_mode()?); - Ok(()) - } -} - impl CtrlGraphics { - pub fn new(config: Arc>) -> std::io::Result { + pub fn new(config: Arc>) -> std::io::Result { let bus = PciBus::new()?; info!("GFX: Rescanning PCI bus"); bus.rescan()?; @@ -108,6 +99,13 @@ impl CtrlGraphics { }) } + /// Force reinit of all state, including reset of device state + pub fn reload(&mut self) -> Result<(), GfxError> { + self.auto_power()?; + info!("GFX: Reloaded gfx mode: {:?}", self.get_gfx_mode()?); + Ok(()) + } + pub fn bus(&self) -> PciBus { self.bus.clone() } @@ -117,7 +115,7 @@ impl CtrlGraphics { } /// Save the selected `Vendor` mode to config - fn save_gfx_mode(vendor: GfxVendors, config: Arc>) { + fn save_gfx_mode(vendor: GfxVendors, config: Arc>) { if let Ok(mut config) = config.lock() { config.gfx_mode = vendor; config.write(); @@ -125,7 +123,7 @@ impl CtrlGraphics { } /// Associated method to get which vendor mode is set - pub(super) fn get_gfx_mode(&self) -> Result { + pub(super) fn get_gfx_mode(&self) -> Result { if let Ok(config) = self.config.lock() { if let Some(mode) = config.gfx_tmp_mode { return Ok(mode); @@ -136,12 +134,12 @@ impl CtrlGraphics { Ok(GfxVendors::Hybrid) } - pub(super) fn get_runtime_status() -> Result { - let path = Path::new("/sys/bus/pci/devices/0000:01:00.0/power/runtime_status"); + pub(super) fn get_runtime_status() -> Result { + let path = Path::new(NVIDIA_RUNTIME_STATUS_PATH); if path.exists() { let buf = std::fs::read_to_string(path).map_err(|err| { - RogError::Read( - "/sys/bus/pci/devices/0000:01:00.0/power/runtime_status".to_string(), + GfxError::Read( + path.to_string_lossy().to_string(), err, ) })?; @@ -152,7 +150,7 @@ impl CtrlGraphics { } /// Some systems have a fallback service to load nouveau if nvidia fails - fn toggle_fallback_service(vendor: GfxVendors) -> Result<(), RogError> { + fn toggle_fallback_service(vendor: GfxVendors) -> Result<(), GfxError> { let action = if vendor == GfxVendors::Nvidia { info!("GFX: Enabling nvidia-fallback.service"); "enable" @@ -165,7 +163,7 @@ impl CtrlGraphics { .arg(action) .arg("nvidia-fallback.service") .status() - .map_err(|err| RogError::Command("systemctl".into(), err))?; + .map_err(|err| GfxError::Command("systemctl".into(), err))?; if !status.success() { // Error is ignored in case this service is removed @@ -179,7 +177,7 @@ impl CtrlGraphics { } /// Write the appropriate xorg config for the chosen mode - fn write_xorg_conf(vendor: GfxVendors) -> Result<(), RogError> { + fn write_xorg_conf(vendor: GfxVendors) -> Result<(), GfxError> { let text = if vendor == GfxVendors::Nvidia { [PRIMARY_GPU_BEGIN, PRIMARY_GPU_NVIDIA, PRIMARY_GPU_END].concat() } else { @@ -187,7 +185,7 @@ impl CtrlGraphics { }; if !Path::new(XORG_PATH).exists() { - std::fs::create_dir(XORG_PATH).map_err(|err| RogError::Write(XORG_PATH.into(), err))?; + std::fs::create_dir(XORG_PATH).map_err(|err| GfxError::Write(XORG_PATH.into(), err))?; } let file = XORG_PATH.to_string().add(XORG_FILE); @@ -197,11 +195,11 @@ impl CtrlGraphics { .truncate(true) .write(true) .open(&file) - .map_err(|err| RogError::Write(file, err))?; + .map_err(|err| GfxError::Write(file, err))?; file.write_all(&text) .and_then(|_| file.sync_all()) - .map_err(|err| RogError::Write(MODPROBE_PATH.into(), err))?; + .map_err(|err| GfxError::Write(MODPROBE_PATH.into(), err))?; Ok(()) } @@ -232,7 +230,7 @@ impl CtrlGraphics { conf } - fn write_modprobe_conf(vendor: GfxVendors, devices: &[GraphicsDevice]) -> Result<(), RogError> { + fn write_modprobe_conf(vendor: GfxVendors, devices: &[GraphicsDevice]) -> Result<(), GfxError> { info!("GFX: Writing {}", MODPROBE_PATH); let content = match vendor { GfxVendors::Nvidia | GfxVendors::Hybrid => { @@ -250,16 +248,16 @@ impl CtrlGraphics { .truncate(true) .write(true) .open(MODPROBE_PATH) - .map_err(|err| RogError::Path(MODPROBE_PATH.into(), err))?; + .map_err(|err| GfxError::Path(MODPROBE_PATH.into(), err))?; file.write_all(&content) .and_then(|_| file.sync_all()) - .map_err(|err| RogError::Write(MODPROBE_PATH.into(), err))?; + .map_err(|err| GfxError::Write(MODPROBE_PATH.into(), err))?; Ok(()) } - fn unbind_remove_nvidia(devices: &[GraphicsDevice]) -> Result<(), RogError> { + fn unbind_remove_nvidia(devices: &[GraphicsDevice]) -> Result<(), GfxError> { // Unbind NVIDIA graphics devices and their functions let unbinds = devices.iter().map(|dev| dev.unbind()); // Remove NVIDIA graphics devices and their functions @@ -267,14 +265,14 @@ impl CtrlGraphics { unbinds .chain(removes) .collect::>() - .map_err(|err| RogError::Command("device unbind error".into(), err)) + .map_err(|err| GfxError::Command("device unbind error".into(), err)) } - fn unbind_only(devices: &[GraphicsDevice]) -> Result<(), RogError> { + fn unbind_only(devices: &[GraphicsDevice]) -> Result<(), GfxError> { let unbinds = devices.iter().map(|dev| dev.unbind()); unbinds .collect::>() - .map_err(|err| RogError::Command("device unbind error".into(), err)) + .map_err(|err| GfxError::Command("device unbind error".into(), err)) } /// Add or remove driver modules @@ -288,7 +286,7 @@ impl CtrlGraphics { if count > MAX_TRIES { let msg = format!("{} {} failed for unknown reason", action, driver); error!("GFX: {}", msg); - return Ok(()); //Err(RogError::Modprobe(msg)); + return Ok(()); //Err(GfxError::Modprobe(msg)); } let output = cmd @@ -337,25 +335,25 @@ impl CtrlGraphics { } } - fn do_display_manager_action(action: &str) -> Result<(), RogError> { + fn do_display_manager_action(action: &str) -> Result<(), GfxError> { let mut cmd = Command::new("systemctl"); cmd.arg(action); cmd.arg(DISPLAY_MANAGER); let status = cmd .status() - .map_err(|err| RogError::Command(format!("{:?}", cmd), err))?; + .map_err(|err| GfxError::Command(format!("{:?}", cmd), err))?; if !status.success() { let msg = format!( "systemctl {} {} failed: {:?}", action, DISPLAY_MANAGER, status ); - return Err(GfxError::DisplayManagerAction(msg, status).into()); + return Err(GfxError::DisplayManagerAction(msg, status)); } Ok(()) } - fn wait_display_manager_state(state: &str) -> Result<(), RogError> { + fn wait_display_manager_state(state: &str) -> Result<(), GfxError> { let mut cmd = Command::new("systemctl"); cmd.arg("is-active"); cmd.arg(DISPLAY_MANAGER); @@ -366,14 +364,14 @@ impl CtrlGraphics { // 3 seconds max let output = cmd .output() - .map_err(|err| RogError::Command(format!("{:?}", cmd), err))?; + .map_err(|err| GfxError::Command(format!("{:?}", cmd), err))?; if output.stdout.starts_with(state.as_bytes()) { return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(250)); count += 1; } - Err(GfxError::DisplayManagerTimeout(state.into()).into()) + Err(GfxError::DisplayManagerTimeout(state.into())) } /// Determine if we need to logout/thread. Integrated<->Vfio mode does not @@ -417,7 +415,7 @@ impl CtrlGraphics { vfio_enable: bool, devices: &[GraphicsDevice], bus: &PciBus, - ) -> Result<(), RogError> { + ) -> Result<(), GfxError> { // Rescan before doing remove or add drivers bus.rescan()?; // Make sure the power management is set to auto for nvidia devices @@ -467,7 +465,7 @@ impl CtrlGraphics { Self::unbind_only(devices)?; Self::do_driver_action("vfio-pci", "modprobe")?; } else { - return Err(GfxError::VfioDisabled.into()); + return Err(GfxError::VfioDisabled); } } GfxVendors::Integrated => { @@ -490,7 +488,7 @@ impl CtrlGraphics { fn graphical_user_sessions_exist( connection: &Connection, sessions: &[SessionInfo], - ) -> Result { + ) -> Result { for session in sessions { let session_proxy = SessionProxy::new(connection, session)?; if session_proxy.get_class()? == SessionClass::User { @@ -514,8 +512,8 @@ impl CtrlGraphics { devices: Vec, bus: PciBus, thread_stop: mpsc::Receiver, - config: Arc>, - ) -> Result { + config: Arc>, + ) -> Result { info!("GFX: display-manager thread started"); const SLEEP_PERIOD: Duration = Duration::from_millis(100); @@ -631,12 +629,12 @@ impl CtrlGraphics { /// to switch modes. /// /// For manually calling (not on boot/startup) via dbus - pub fn set_gfx_mode(&mut self, vendor: GfxVendors) -> Result { - if let Ok(gsync) = CtrlRogBios::get_gfx_mode() { - if gsync == 1 { - return Err(GfxError::GsyncModeActive.into()); - } - } + pub fn set_gfx_mode(&mut self, vendor: GfxVendors) -> Result { + // if let Ok(gsync) = CtrlRogBios::get_gfx_mode() { + // if gsync == 1 { + // return Err(GfxError::GsyncModeActive.into()); + // } + // } let vfio_enable = if let Ok(config) = self.config.try_lock() { config.gfx_vfio_enable @@ -645,7 +643,7 @@ impl CtrlGraphics { }; if !vfio_enable && matches!(vendor, GfxVendors::Vfio) { - return Err(GfxError::VfioDisabled.into()); + return Err(GfxError::VfioDisabled); } // Must always cancel any thread running @@ -687,7 +685,7 @@ impl CtrlGraphics { } /// Used only on boot to set correct mode - fn auto_power(&mut self) -> Result<(), RogError> { + fn auto_power(&mut self) -> Result<(), GfxError> { let vendor = self.get_gfx_mode()?; let devices = self.nvidia.clone(); let bus = self.bus.clone(); diff --git a/daemon/src/ctrl_gfx/error.rs b/supergfx/src/error.rs similarity index 71% rename from daemon/src/ctrl_gfx/error.rs rename to supergfx/src/error.rs index ec765653..a43d9445 100644 --- a/daemon/src/ctrl_gfx/error.rs +++ b/supergfx/src/error.rs @@ -1,8 +1,6 @@ use std::fmt; use std::{error, process::ExitStatus}; -use crate::error::RogError; - #[derive(Debug)] pub enum GfxError { ParseVendor, @@ -16,6 +14,11 @@ pub enum GfxError { MissingModule(String), Modprobe(String), Command(String, std::io::Error), + Path(String, std::io::Error), + Read(String, std::io::Error), + Write(String, std::io::Error), + Io(std::io::Error), + Zbus(zbus::Error), } impl fmt::Display for GfxError { @@ -45,14 +48,25 @@ impl fmt::Display for GfxError { GfxError::MissingModule(m) => write!(f, "The module {} is missing", m), GfxError::Modprobe(detail) => write!(f, "Modprobe error: {}", detail), GfxError::Command(func, error) => write!(f, "Command exec error: {}: {}", func, error), + GfxError::Path(path, error) => write!(f, "Path {}: {}", path, error), + GfxError::Read(path, error) => write!(f, "Read {}: {}", path, error), + GfxError::Write(path, error) => write!(f, "Write {}: {}", path, error), + GfxError::Io(detail) => write!(f, "std::io error: {}", detail), + GfxError::Zbus(detail) => write!(f, "Zbus error: {}", detail), } } } impl error::Error for GfxError {} -impl From for RogError { - fn from(err: GfxError) -> Self { - RogError::GfxSwitching(err) +impl From for GfxError { + fn from(err: zbus::Error) -> Self { + GfxError::Zbus(err) } } + +impl From for GfxError { + fn from(err: std::io::Error) -> Self { + GfxError::Io(err) + } +} \ No newline at end of file diff --git a/rog-types/src/gfx_vendors.rs b/supergfx/src/gfx_vendors.rs similarity index 90% rename from rog-types/src/gfx_vendors.rs rename to supergfx/src/gfx_vendors.rs index 86636b55..f16200a8 100644 --- a/rog-types/src/gfx_vendors.rs +++ b/supergfx/src/gfx_vendors.rs @@ -1,8 +1,9 @@ -use crate::error::GraphicsError; use serde_derive::{Deserialize, Serialize}; use std::str::FromStr; use zvariant_derive::Type; +use crate::error::GfxError; + #[derive(Debug, Type, PartialEq, Copy, Clone, Deserialize, Serialize)] pub enum GfxPower { Active, @@ -12,9 +13,9 @@ pub enum GfxPower { } impl FromStr for GfxPower { - type Err = GraphicsError; + type Err = GfxError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s.to_lowercase().trim() { "active" => Ok(GfxPower::Active), "suspended" => Ok(GfxPower::Suspended), @@ -45,9 +46,9 @@ pub enum GfxVendors { } impl FromStr for GfxVendors { - type Err = GraphicsError; + type Err = GfxError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "nvidia" => Ok(GfxVendors::Nvidia), "hybrid" => Ok(GfxVendors::Hybrid), @@ -59,7 +60,7 @@ impl FromStr for GfxVendors { "compute\n" => Ok(GfxVendors::Compute), "vfio\n" => Ok(GfxVendors::Vfio), "integrated\n" => Ok(GfxVendors::Integrated), - _ => Err(GraphicsError::ParseVendor), + _ => Err(GfxError::ParseVendor), } } } diff --git a/daemon/src/ctrl_gfx/mod.rs b/supergfx/src/lib.rs similarity index 97% rename from daemon/src/ctrl_gfx/mod.rs rename to supergfx/src/lib.rs index 183fc2a9..527667c9 100644 --- a/daemon/src/ctrl_gfx/mod.rs +++ b/supergfx/src/lib.rs @@ -1,9 +1,8 @@ pub mod error; - +pub mod config; +pub mod gfx_vendors; pub mod controller; - pub mod system; - pub mod zbus; const NVIDIA_DRIVERS: [&str; 4] = ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia"]; diff --git a/daemon/src/ctrl_gfx/system.rs b/supergfx/src/system.rs similarity index 100% rename from daemon/src/ctrl_gfx/system.rs rename to supergfx/src/system.rs diff --git a/supergfx/src/zbus.rs b/supergfx/src/zbus.rs new file mode 100644 index 00000000..18551f33 --- /dev/null +++ b/supergfx/src/zbus.rs @@ -0,0 +1,55 @@ +use log::{error, info, warn}; +use zvariant::ObjectPath; +use ::zbus::dbus_interface; + +use crate::{gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}}; + +use super::controller::CtrlGraphics; + +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlGraphics { + fn vendor(&self) -> zbus::fdo::Result { + self.get_gfx_mode().map_err(|err| { + error!("GFX: {}", err); + zbus::fdo::Error::Failed(format!("GFX fail: {}", err)) + }) + } + + fn power(&self) -> zbus::fdo::Result { + Self::get_runtime_status().map_err(|err| { + error!("GFX: {}", err); + zbus::fdo::Error::Failed(format!("GFX fail: {}", err)) + }) + } + + fn set_vendor(&mut self, vendor: GfxVendors) -> zbus::fdo::Result { + info!("GFX: Switching gfx mode to {}", <&str>::from(vendor)); + let msg = self.set_gfx_mode(vendor).map_err(|err| { + error!("GFX: {}", err); + zbus::fdo::Error::Failed(format!("GFX fail: {}", err)) + })?; + self.notify_gfx(&vendor) + .unwrap_or_else(|err| warn!("GFX: {}", err)); + self.notify_action(&msg) + .unwrap_or_else(|err| warn!("GFX: {}", err)); + Ok(msg) + } + + #[dbus_interface(signal)] + fn notify_gfx(&self, vendor: &GfxVendors) -> zbus::Result<()> {} + + #[dbus_interface(signal)] + fn notify_action(&self, action: &GfxRequiredUserAction) -> zbus::Result<()> {} +} + +impl CtrlGraphics { + pub fn add_to_server(self, server: &mut zbus::ObjectServer) { + server + .at(&ObjectPath::from_str_unchecked("/org/asuslinux/Gfx"), self) + .map_err(|err| { + warn!("GFX: CtrlGraphics: add_to_server {}", err); + err + }) + .ok(); + } +}