From 4611c08085419c59eb749c09ffbd6560c2457040 Mon Sep 17 00:00:00 2001 From: "Luke D. Jones" Date: Sat, 24 Jun 2023 13:15:11 +1200 Subject: [PATCH] Add the missing dirs, dumbarse --- asusd-user/Cargo.toml | 35 ++ asusd-user/README.md | 14 + asusd-user/src/config.rs | 219 ++++++++++ asusd-user/src/ctrl_anime.rs | 373 +++++++++++++++++ asusd-user/src/daemon.rs | 116 +++++ asusd-user/src/error.rs | 45 ++ asusd-user/src/lib.rs | 11 + asusd-user/src/zbus_anime.rs | 70 ++++ asusd/Cargo.toml | 46 ++ asusd/src/config.rs | 77 ++++ asusd/src/ctrl_anime/config.rs | 214 ++++++++++ asusd/src/ctrl_anime/mod.rs | 261 ++++++++++++ asusd/src/ctrl_anime/trait_impls.rs | 299 +++++++++++++ asusd/src/ctrl_aura/config.rs | 357 ++++++++++++++++ asusd/src/ctrl_aura/controller.rs | 558 +++++++++++++++++++++++++ asusd/src/ctrl_aura/mod.rs | 4 + asusd/src/ctrl_aura/trait_impls.rs | 331 +++++++++++++++ asusd/src/ctrl_platform.rs | 382 +++++++++++++++++ asusd/src/ctrl_power.rs | 287 +++++++++++++ asusd/src/ctrl_profiles/config.rs | 60 +++ asusd/src/ctrl_profiles/controller.rs | 191 +++++++++ asusd/src/ctrl_profiles/mod.rs | 4 + asusd/src/ctrl_profiles/trait_impls.rs | 311 ++++++++++++++ asusd/src/ctrl_supported.rs | 42 ++ asusd/src/daemon.rs | 165 ++++++++ asusd/src/error.rs | 135 ++++++ asusd/src/lib.rs | 229 ++++++++++ simulators/src/animatrix/map_ga401.rs | 14 + simulators/src/animatrix/map_ga402.rs | 15 + simulators/src/animatrix/map_gu604.rs | 14 + simulators/src/animatrix/mod.rs | 75 ++++ 31 files changed, 4954 insertions(+) create mode 100644 asusd-user/Cargo.toml create mode 100644 asusd-user/README.md create mode 100644 asusd-user/src/config.rs create mode 100644 asusd-user/src/ctrl_anime.rs create mode 100644 asusd-user/src/daemon.rs create mode 100644 asusd-user/src/error.rs create mode 100644 asusd-user/src/lib.rs create mode 100644 asusd-user/src/zbus_anime.rs create mode 100644 asusd/Cargo.toml create mode 100644 asusd/src/config.rs create mode 100644 asusd/src/ctrl_anime/config.rs create mode 100644 asusd/src/ctrl_anime/mod.rs create mode 100644 asusd/src/ctrl_anime/trait_impls.rs create mode 100644 asusd/src/ctrl_aura/config.rs create mode 100644 asusd/src/ctrl_aura/controller.rs create mode 100644 asusd/src/ctrl_aura/mod.rs create mode 100644 asusd/src/ctrl_aura/trait_impls.rs create mode 100644 asusd/src/ctrl_platform.rs create mode 100644 asusd/src/ctrl_power.rs create mode 100644 asusd/src/ctrl_profiles/config.rs create mode 100644 asusd/src/ctrl_profiles/controller.rs create mode 100644 asusd/src/ctrl_profiles/mod.rs create mode 100644 asusd/src/ctrl_profiles/trait_impls.rs create mode 100644 asusd/src/ctrl_supported.rs create mode 100644 asusd/src/daemon.rs create mode 100644 asusd/src/error.rs create mode 100644 asusd/src/lib.rs create mode 100644 simulators/src/animatrix/map_ga401.rs create mode 100644 simulators/src/animatrix/map_ga402.rs create mode 100644 simulators/src/animatrix/map_gu604.rs create mode 100644 simulators/src/animatrix/mod.rs diff --git a/asusd-user/Cargo.toml b/asusd-user/Cargo.toml new file mode 100644 index 00000000..84c69255 --- /dev/null +++ b/asusd-user/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "asusd-user" +license = "MPL-2.0" +version.workspace = true +authors = ["Luke D Jones "] +edition = "2021" +description = "Usermode daemon for user settings, anime, per-key lighting" + +[[bin]] +name = "asusd-user" +path = "src/daemon.rs" + +[dependencies] +dirs.workspace = true +smol.workspace = true + +# serialisation +serde.workspace = true +serde_json.workspace = true +serde_derive.workspace = true + +rog_anime = { path = "../rog-anime" } +rog_aura = { path = "../rog-aura" } +rog_dbus = { path = "../rog-dbus" } +rog_platform = { path = "../rog-platform" } +config-traits = { path = "../config-traits" } + +zbus.workspace = true + +# cli and logging +log.workspace = true +env_logger.workspace = true + +[dev-dependencies] +cargo-husky.workspace = true \ No newline at end of file diff --git a/asusd-user/README.md b/asusd-user/README.md new file mode 100644 index 00000000..be3b4e23 --- /dev/null +++ b/asusd-user/README.md @@ -0,0 +1,14 @@ +# daemon-user + +This crate is for the binary of `asusd-user` and its helper lib. + +The purpose of `asusd-user` is to run in userland and provide the user + third-party apps an interface for such things as creating AniMe sequences (and more in future, see todo list). + +`asusd-user` should try to be as simple as possible while allowing a decent degree of control. + +## TODO + +- [ ] CLI for basic settings/interaction +- [ ] RGB keyboard per-key programs +- [ ] User profiles (fan, cpu etc). These would be replacing the system-daemon profiles only when the user is active, otherwise system-daemon defaults to system settings. +- [ ] Audio EQ visualiser - for use with anime + keyboard lighting \ No newline at end of file diff --git a/asusd-user/src/config.rs b/asusd-user/src/config.rs new file mode 100644 index 00000000..785bfbf1 --- /dev/null +++ b/asusd-user/src/config.rs @@ -0,0 +1,219 @@ +use std::path::PathBuf; +use std::time::Duration; + +use config_traits::{StdConfig, StdConfigLoad}; +use rog_anime::{ActionLoader, AnimTime, AnimeType, Fade, Sequences as AnimeSequences, Vec2}; +use rog_aura::advanced::LedCode; +use rog_aura::effects::{AdvancedEffects as AuraSequences, Breathe, DoomFlicker, Effect, Static}; +use rog_aura::{Colour, Speed}; +use serde_derive::{Deserialize, Serialize}; + +use crate::error::Error; + +const ROOT_CONF_DIR: &str = "rog"; + +fn root_conf_dir() -> PathBuf { + let mut dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("/tmp")); + dir.push(ROOT_CONF_DIR); + dir +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ConfigAnime { + pub name: String, + pub anime: Vec, +} + +impl ConfigAnime { + pub fn create(&self, anime_type: AnimeType) -> Result { + let mut seq = AnimeSequences::new(anime_type); + + for (idx, action) in self.anime.iter().enumerate() { + seq.insert(idx, action)?; + } + + Ok(seq) + } + + pub fn set_name(mut self, name: String) -> Self { + self.name = name; + self + } +} + +impl Default for ConfigAnime { + fn default() -> Self { + Self { + name: "anime-default".to_owned(), + anime: vec![ + ActionLoader::AsusImage { + file: "/usr/share/asusd/anime/custom/diagonal-template.png".into(), + brightness: 1.0, + time: AnimTime::Fade(Fade::new( + Duration::from_secs(2), + None, + Duration::from_secs(2), + )), + }, + ActionLoader::AsusAnimation { + file: "/usr/share/asusd/anime/asus/rog/Sunset.gif".into(), + brightness: 0.5, + time: AnimTime::Fade(Fade::new( + Duration::from_secs(6), + None, + Duration::from_secs(3), + )), + }, + ActionLoader::ImageAnimation { + file: "/usr/share/asusd/anime/custom/sonic-run.gif".into(), + scale: 0.9, + angle: 0.65, + translation: Vec2::default(), + brightness: 0.5, + time: AnimTime::Fade(Fade::new( + Duration::from_secs(2), + Some(Duration::from_secs(2)), + Duration::from_secs(2), + )), + }, + ActionLoader::Image { + file: "/usr/share/asusd/anime/custom/rust.png".into(), + scale: 1.0, + angle: 0.0, + translation: Vec2::default(), + time: AnimTime::Fade(Fade::new( + Duration::from_secs(2), + Some(Duration::from_secs(1)), + Duration::from_secs(2), + )), + brightness: 0.6, + }, + ActionLoader::Pause(Duration::from_secs(1)), + 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: 0.5, + time: AnimTime::Count(2), + }, + ], + } + } +} + +impl StdConfig for ConfigAnime { + fn new() -> Self { + Self::default() + } + + fn file_name(&self) -> String { + format!("{}.ron", self.name) + } + + fn config_dir() -> std::path::PathBuf { + root_conf_dir() + } +} + +impl StdConfigLoad for ConfigAnime {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ConfigAura { + pub name: String, + pub aura: AuraSequences, +} + +impl ConfigAura { + pub fn set_name(mut self, name: String) -> Self { + self.name = name; + self + } +} + +impl Default for ConfigAura { + fn default() -> Self { + let mut seq = AuraSequences::new(false); + let mut key = Effect::Breathe(Breathe::new( + LedCode::W, + Colour(255, 0, 20), + Colour(20, 255, 0), + Speed::Low, + )); + + seq.push(key.clone()); + key.set_led(LedCode::A); + seq.push(key.clone()); + key.set_led(LedCode::S); + seq.push(key.clone()); + key.set_led(LedCode::D); + seq.push(key); + + let key = Effect::Breathe(Breathe::new( + LedCode::F, + Colour(255, 0, 0), + Colour(255, 0, 0), + Speed::High, + )); + seq.push(key); + + let mut key = Effect::Static(Static::new(LedCode::RCtrl, Colour(0, 0, 255))); + seq.push(key.clone()); + key.set_led(LedCode::LCtrl); + seq.push(key.clone()); + key.set_led(LedCode::Esc); + seq.push(key); + + let key = Effect::DoomFlicker(DoomFlicker::new(LedCode::N9, Colour(0, 0, 255), 80, 40)); + seq.push(key); + + Self { + name: "aura-default".to_owned(), + aura: seq, + } + } +} + +impl StdConfig for ConfigAura { + fn new() -> Self { + Self::default() + } + + fn file_name(&self) -> String { + format!("{}.ron", self.name) + } + + fn config_dir() -> std::path::PathBuf { + root_conf_dir() + } +} + +impl StdConfigLoad for ConfigAura {} + +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(default)] +pub struct ConfigBase { + /// Name of active anime config file in the user config directory + pub active_anime: Option, + /// Name of active aura config file in the user config directory + pub active_aura: Option, +} + +impl StdConfig for ConfigBase { + fn new() -> Self { + Self { + active_anime: Some("anime-default".to_owned()), + active_aura: Some("aura-default".to_owned()), + } + } + + fn file_name(&self) -> String { + "rog-user.ron".to_owned() + } + + fn config_dir() -> std::path::PathBuf { + root_conf_dir() + } +} + +impl StdConfigLoad for ConfigBase {} diff --git a/asusd-user/src/ctrl_anime.rs b/asusd-user/src/ctrl_anime.rs new file mode 100644 index 00000000..a87898d2 --- /dev/null +++ b/asusd-user/src/ctrl_anime.rs @@ -0,0 +1,373 @@ +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +use config_traits::StdConfig; +use rog_anime::error::AnimeError; +use rog_anime::{ActionData, ActionLoader, AnimTime, Fade, Sequences, Vec2}; +use rog_dbus::RogDbusClientBlocking; +use serde_derive::{Deserialize, Serialize}; +use zbus::dbus_interface; +use zbus::zvariant::{ObjectPath, Type}; + +use crate::config::ConfigAnime; +use crate::error::Error; + +#[derive(Debug, Clone, Deserialize, Serialize, Type)] +pub struct Timer { + type_of: TimeType, + /// If time type is Timer then this is milliseonds, otherwise it is + /// animation loop count + count: u64, + /// Used only for `TimeType::Timer`, milliseonds to fade the image in for + fade_in: Option, + /// Used only for `TimeType::Timer`, milliseonds to fade the image out for + fade_out: Option, +} + +impl From for AnimTime { + fn from(time: Timer) -> Self { + match time.type_of { + TimeType::Timer => { + if time.fade_in.is_some() || time.fade_out.is_some() { + let fade_in = time + .fade_in + .map_or(Duration::from_secs(0), Duration::from_millis); + let fade_out = time + .fade_out + .map_or(Duration::from_secs(0), Duration::from_millis); + let show_for = if time.count != 0 { + Some(Duration::from_millis(time.count)) + } else { + None + }; + AnimTime::Fade(Fade::new(fade_in, show_for, fade_out)) + } else { + AnimTime::Time(Duration::from_millis(time.count)) + } + } + TimeType::Count => AnimTime::Count(time.count as u32), + TimeType::Infinite => AnimTime::Infinite, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Type)] +pub enum TimeType { + Timer, + Count, + Infinite, +} + +/// The inner object exists to allow the zbus proxy to share it with a runner +/// thread and a zbus server behind `Arc>` +pub struct CtrlAnimeInner<'a> { + sequences: Sequences, + client: RogDbusClientBlocking<'a>, + do_early_return: Arc, +} + +impl<'a> CtrlAnimeInner<'static> { + pub fn new( + sequences: Sequences, + client: RogDbusClientBlocking<'static>, + do_early_return: Arc, + ) -> Result { + Ok(Self { + sequences, + client, + do_early_return, + }) + } + + /// To be called on each main loop iteration to pump out commands to the + /// anime + pub fn run(&'a self) -> Result<(), Error> { + if self.do_early_return.load(Ordering::SeqCst) { + return Ok(()); + } + + for action in self.sequences.iter() { + match action { + ActionData::Animation(frames) => { + rog_anime::run_animation(frames, &|output| { + if self.do_early_return.load(Ordering::Acquire) { + return Ok(true); // Do safe exit + } + self.client + .proxies() + .anime() + .write(output) + .map_err(|e| AnimeError::Dbus(format!("{}", e))) + .map(|_| false) + }); + } + ActionData::Image(image) => { + self.client + .proxies() + .anime() + .write(image.as_ref().clone()) + .ok(); + } + ActionData::Pause(duration) => { + let start = Instant::now(); + 'pause: loop { + if self.do_early_return.load(Ordering::SeqCst) { + return Ok(()); + } + if Instant::now().duration_since(start) > *duration { + break 'pause; + } + sleep(Duration::from_millis(1)); + } + } + ActionData::AudioEq + | ActionData::SystemInfo + | ActionData::TimeDate + | ActionData::Matrix => {} + } + } + + Ok(()) + } +} + +pub struct CtrlAnime<'a> { + config: Arc>, + client: RogDbusClientBlocking<'a>, + inner: Arc>>, + /// Must be the same Atomic as in CtrlAnimeInner + inner_early_return: Arc, +} + +impl CtrlAnime<'static> { + pub fn new( + config: Arc>, + inner: Arc>>, + client: RogDbusClientBlocking<'static>, + inner_early_return: Arc, + ) -> Result { + Ok(CtrlAnime { + config, + client, + inner, + inner_early_return, + }) + } + + pub async fn add_to_server(self, server: &mut zbus::Connection) { + server + .object_server() + .at( + &ObjectPath::from_str_unchecked("/org/asuslinux/Anime"), + self, + ) + .await + .map_err(|err| { + println!("CtrlAnime: add_to_server {}", err); + err + }) + .ok(); + } +} + +// The pattern for a zbus method is: +// - Get config lock if required +// - Set inner_early_return to stop the inner run loop temporarily +// - Do actions +// - Write config if required +// - Unset inner_early_return +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlAnime<'static> { + pub fn insert_asus_gif( + &mut self, + index: u32, + file: &str, + time: Timer, + brightness: f32, + ) -> zbus::fdo::Result { + if let Ok(mut config) = self.config.try_lock() { + let time: AnimTime = time.into(); + let file = Path::new(&file); + let action = ActionLoader::AsusAnimation { + file: file.into(), + brightness, + time, + }; + + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + + if let Ok(mut controller) = self.inner.lock() { + controller + .sequences + .insert(index as usize, &action) + .map_err(|err| zbus::fdo::Error::Failed(err.to_string()))?; + } + config.anime.push(action); + config.write(); + + let json = serde_json::to_string_pretty(&*config).expect("Parse config to JSON failed"); + + // Release the inner run loop again + self.inner_early_return.store(false, Ordering::SeqCst); + return Ok(json); + } + Err(zbus::fdo::Error::Failed("UserConfig lock fail".into())) + } + + #[allow(clippy::too_many_arguments)] + pub fn insert_image_gif( + &mut self, + index: u32, + file: &str, + scale: f32, + angle: f32, + xy: (f32, f32), + time: Timer, + brightness: f32, + ) -> zbus::fdo::Result { + if let Ok(mut config) = self.config.try_lock() { + let time: AnimTime = time.into(); + let file = Path::new(&file); + let translation = Vec2::new(xy.0, xy.1); + let action = ActionLoader::ImageAnimation { + file: file.into(), + scale, + angle, + translation, + brightness, + time, + }; + + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + + if let Ok(mut controller) = self.inner.lock() { + controller + .sequences + .insert(index as usize, &action) + .map_err(|err| zbus::fdo::Error::Failed(err.to_string()))?; + } + config.anime.push(action); + config.write(); + + let json = + serde_json::to_string_pretty(&*config.anime).expect("Parse config to JSON failed"); + + // Release the inner run loop again + self.inner_early_return.store(false, Ordering::SeqCst); + return Ok(json); + } + Err(zbus::fdo::Error::Failed("UserConfig lock fail".into())) + } + + #[allow(clippy::too_many_arguments)] + pub fn insert_image( + &mut self, + index: u32, + file: &str, + scale: f32, + angle: f32, + xy: (f32, f32), + time: Timer, + brightness: f32, + ) -> zbus::fdo::Result { + if let Ok(mut config) = self.config.try_lock() { + let file = Path::new(&file); + let time = time.into(); + let action = ActionLoader::Image { + file: file.into(), + scale, + angle, + translation: Vec2::new(xy.0, xy.1), + brightness, + time, + }; + + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + + if let Ok(mut controller) = self.inner.lock() { + controller + .sequences + .insert(index as usize, &action) + .map_err(|err| zbus::fdo::Error::Failed(err.to_string()))?; + } + config.anime.push(action); + config.write(); + + let json = + serde_json::to_string_pretty(&*config.anime).expect("Parse config to JSON failed"); + + // Release the inner run loop again + self.inner_early_return.store(false, Ordering::SeqCst); + return Ok(json); + } + Err(zbus::fdo::Error::Failed("UserConfig lock fail".into())) + } + + pub fn insert_pause(&mut self, index: u32, millis: u64) -> zbus::fdo::Result { + if let Ok(mut config) = self.config.try_lock() { + let action = ActionLoader::Pause(Duration::from_millis(millis)); + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + + if let Ok(mut controller) = self.inner.lock() { + controller + .sequences + .insert(index as usize, &action) + .map_err(|err| zbus::fdo::Error::Failed(err.to_string()))?; + } + config.anime.push(action); + config.write(); + + let json = + serde_json::to_string_pretty(&*config.anime).expect("Parse config to JSON failed"); + + // Release the inner run loop again + self.inner_early_return.store(false, Ordering::SeqCst); + return Ok(json); + } + Err(zbus::fdo::Error::Failed("UserConfig lock fail".into())) + } + + pub fn remove_item(&mut self, index: u32) -> zbus::fdo::Result { + if let Ok(mut config) = self.config.try_lock() { + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + + if let Ok(mut controller) = self.inner.lock() { + controller.sequences.remove_item(index as usize); + } + if (index as usize) < config.anime.len() { + config.anime.remove(index as usize); + } + config.write(); + + let json = + serde_json::to_string_pretty(&*config.anime).expect("Parse config to JSON failed"); + + // Release the inner run loop again + self.inner_early_return.store(false, Ordering::SeqCst); + return Ok(json); + } + Err(zbus::fdo::Error::Failed("UserConfig lock fail".into())) + } + + pub fn set_state(&mut self, on: bool) -> zbus::fdo::Result<()> { + // Operations here need to be in specific order + if on { + self.client.proxies().anime().set_enable_display(on).ok(); + // Let the inner loop run + self.inner_early_return.store(false, Ordering::SeqCst); + } else { + // Must make the inner run loop return early + self.inner_early_return.store(true, Ordering::SeqCst); + self.client.proxies().anime().set_enable_display(on).ok(); + } + Ok(()) + } +} diff --git a/asusd-user/src/daemon.rs b/asusd-user/src/daemon.rs new file mode 100644 index 00000000..d012de46 --- /dev/null +++ b/asusd-user/src/daemon.rs @@ -0,0 +1,116 @@ +use std::io::Write; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; + +use asusd_user::config::*; +use asusd_user::ctrl_anime::{CtrlAnime, CtrlAnimeInner}; +use asusd_user::DBUS_NAME; +use config_traits::{StdConfig, StdConfigLoad}; +use rog_anime::usb::get_anime_type; +use rog_aura::aura_detection::LaptopLedData; +use rog_aura::layouts::KeyLayout; +use rog_dbus::RogDbusClientBlocking; +use smol::Executor; +use zbus::Connection; + +#[cfg(not(feature = "local_data"))] +const DATA_DIR: &str = "/usr/share/rog-gui/"; +#[cfg(feature = "local_data")] +const DATA_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const BOARD_NAME: &str = "/sys/class/dmi/id/board_name"; + +fn main() -> Result<(), Box> { + let mut logger = env_logger::Builder::new(); + logger + .parse_default_env() + .target(env_logger::Target::Stdout) + .format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args())) + .init(); + + println!(" user daemon v{}", asusd_user::VERSION); + println!(" rog-anime v{}", rog_anime::VERSION); + println!(" rog-dbus v{}", rog_dbus::VERSION); + println!("rog-platform v{}", rog_platform::VERSION); + + let (client, _) = RogDbusClientBlocking::new()?; + let supported = client.proxies().supported().supported_functions()?; + + let config = ConfigBase::new().load(); + + let executor = Executor::new(); + + let early_return = Arc::new(AtomicBool::new(false)); + // Set up the anime data and run loop/thread + if supported.anime_ctrl.0 { + if let Some(cfg) = config.active_anime { + let anime_type = get_anime_type()?; + let anime_config = ConfigAnime::new().set_name(cfg).load(); + let anime = anime_config.create(anime_type)?; + let anime_config = Arc::new(Mutex::new(anime_config)); + + executor + .spawn(async move { + // Create server + let mut connection = Connection::session().await.unwrap(); + connection.request_name(DBUS_NAME).await.unwrap(); + + // Inner behind mutex required for thread safety + let inner = Arc::new(Mutex::new( + CtrlAnimeInner::new(anime, client, early_return.clone()).unwrap(), + )); + // Need new client object for dbus control part + let (client, _) = RogDbusClientBlocking::new().unwrap(); + let anime_control = + CtrlAnime::new(anime_config, inner.clone(), client, early_return).unwrap(); + anime_control.add_to_server(&mut connection).await; + loop { + if let Ok(inner) = inner.clone().try_lock() { + inner.run().ok(); + } + } + }) + .detach(); + } + } + + // if supported.keyboard_led.per_key_led_mode { + if let Some(cfg) = config.active_aura { + let mut aura_config = ConfigAura::new().set_name(cfg).load(); + // let baord_name = std::fs::read_to_string(BOARD_NAME)?; + + let led_support = LaptopLedData::get_data(); + + let layout = KeyLayout::find_layout(led_support, PathBuf::from(DATA_DIR)) + .map_err(|e| { + println!("{BOARD_NAME}, {e}"); + }) + .unwrap_or_else(|_| KeyLayout::default_layout()); + + executor + .spawn(async move { + // Create server + let (client, _) = RogDbusClientBlocking::new().unwrap(); + // let connection = Connection::session().await.unwrap(); + // connection.request_name(DBUS_NAME).await.unwrap(); + + loop { + aura_config.aura.next_state(&layout); + let packets = aura_config.aura.create_packets(); + + client + .proxies() + .led() + .direct_addressing_raw(packets) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(33)); + } + }) + .detach(); + } + // } + + loop { + smol::block_on(executor.tick()); + } +} diff --git a/asusd-user/src/error.rs b/asusd-user/src/error.rs new file mode 100644 index 00000000..e6c0e33e --- /dev/null +++ b/asusd-user/src/error.rs @@ -0,0 +1,45 @@ +use std::fmt; + +use rog_anime::error::AnimeError; + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + ConfigLoadFail, + ConfigLockFail, + XdgVars, + Anime(AnimeError), +} + +impl fmt::Display for Error { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Io(err) => write!(f, "Failed to open: {}", err), + Error::ConfigLoadFail => write!(f, "Failed to load user config"), + Error::ConfigLockFail => write!(f, "Failed to lock user config"), + Error::XdgVars => write!(f, "XDG environment vars appear unset"), + Error::Anime(err) => write!(f, "Anime error: {}", err), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: AnimeError) -> Self { + Error::Anime(err) + } +} + +impl From for zbus::fdo::Error { + fn from(err: Error) -> Self { + zbus::fdo::Error::Failed(format!("Anime zbus error: {}", err)) + } +} diff --git a/asusd-user/src/lib.rs b/asusd-user/src/lib.rs new file mode 100644 index 00000000..25a75409 --- /dev/null +++ b/asusd-user/src/lib.rs @@ -0,0 +1,11 @@ +pub mod config; + +pub mod error; + +pub mod ctrl_anime; + +pub mod zbus_anime; + +pub static DBUS_NAME: &str = "org.asuslinux.Daemon"; + +pub static VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/asusd-user/src/zbus_anime.rs b/asusd-user/src/zbus_anime.rs new file mode 100644 index 00000000..793f373b --- /dev/null +++ b/asusd-user/src/zbus_anime.rs @@ -0,0 +1,70 @@ +//! # `DBus` interface proxy for: `org.asuslinux.Daemon` +//! +//! This code was generated by `zbus-xmlgen` `1.0.0` from `DBus` introspection +//! data. Source: `Interface '/org/asuslinux/Anime' from service +//! 'org.asuslinux.Daemon' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This `DBus` object implements +//! [standard `DBus` interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. +#![allow(clippy::too_many_arguments)] + +use zbus::dbus_proxy; + +#[dbus_proxy(interface = "org.asuslinux.Daemon")] +trait Daemon { + /// InsertAsusGif method + fn insert_asus_gif( + &self, + index: u32, + file: &str, + time: u32, + count: u32, + brightness: f64, + ) -> zbus::Result; + + /// InsertImage method + fn insert_image( + &self, + index: u32, + file: &str, + scale: f64, + angle: f64, + xy: &(f64, f64), + brightness: f64, + ) -> zbus::Result; + + /// InsertImageGif method + fn insert_image_gif( + &self, + index: u32, + file: &str, + scale: f64, + angle: f64, + xy: &(f64, f64), + time: u32, + count: u32, + brightness: f64, + ) -> zbus::Result; + + /// InsertPause method + fn insert_pause(&self, index: u32, millis: u64) -> zbus::Result; + + /// RemoveItem method + fn remove_item(&self, index: u32) -> zbus::Result; + + /// SetState method + fn set_state(&self, on: bool) -> zbus::Result<()>; +} diff --git a/asusd/Cargo.toml b/asusd/Cargo.toml new file mode 100644 index 00000000..e0ac5442 --- /dev/null +++ b/asusd/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "asusd" +license = "MPL-2.0" +version.workspace = true +readme = "README.md" +authors = ["Luke "] +repository = "https://gitlab.com/asus-linux/asus-nb-ctrl" +homepage = "https://gitlab.com/asus-linux/asus-nb-ctrl" +description = "A daemon app for ASUS GX502 and similar laptops to control missing features" +edition = "2021" + +[[bin]] +name = "asusd" +path = "src/daemon.rs" + +[dependencies] +config-traits = { path = "../config-traits" } +rog_anime = { path = "../rog-anime", features = ["dbus"] } +rog_aura = { path = "../rog-aura", features = ["dbus"] } +rog_platform = { path = "../rog-platform" } +rog_profiles = { path = "../rog-profiles" } +rog_dbus = { path = "../rog-dbus" } + +async-trait.workspace = true +tokio.workspace = true + +# cli and logging +log.workspace = true +env_logger.workspace = true + +zbus.workspace = true +logind-zbus.workspace = true + +# serialisation +serde.workspace = true +serde_derive.workspace = true + +# Device control +sysfs-class.workspace = true # used for backlight control and baord ID + +concat-idents.workspace = true + +systemd-zbus = "*" + +[dev-dependencies] +cargo-husky.workspace = true \ No newline at end of file diff --git a/asusd/src/config.rs b/asusd/src/config.rs new file mode 100644 index 00000000..a1669651 --- /dev/null +++ b/asusd/src/config.rs @@ -0,0 +1,77 @@ +use config_traits::{StdConfig, StdConfigLoad2}; +use serde_derive::{Deserialize, Serialize}; + +const CONFIG_FILE: &str = "asusd.ron"; + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct Config { + /// Save charge limit for restoring on boot + pub bat_charge_limit: u8, + pub panel_od: bool, + pub disable_nvidia_powerd_on_battery: bool, + pub ac_command: String, + pub bat_command: String, +} + +impl StdConfig for Config { + fn new() -> Self { + Config { + bat_charge_limit: 100, + panel_od: false, + disable_nvidia_powerd_on_battery: true, + ac_command: String::new(), + bat_command: String::new(), + } + } + + fn config_dir() -> std::path::PathBuf { + std::path::PathBuf::from(crate::CONFIG_PATH_BASE) + } + + fn file_name(&self) -> String { + CONFIG_FILE.to_owned() + } +} + +impl StdConfigLoad2 for Config {} + +#[derive(Deserialize, Serialize, Default)] +#[serde(default)] +pub struct Config455 { + /// Save charge limit for restoring on boot + pub bat_charge_limit: u8, + pub panel_od: bool, +} + +impl From for Config { + fn from(c: Config455) -> Self { + Self { + bat_charge_limit: c.bat_charge_limit, + panel_od: c.panel_od, + disable_nvidia_powerd_on_battery: true, + ac_command: String::new(), + bat_command: String::new(), + } + } +} + +#[derive(Deserialize, Serialize, Default)] +pub struct Config458 { + /// Save charge limit for restoring on boot + pub bat_charge_limit: u8, + pub panel_od: bool, + pub ac_command: String, + pub bat_command: String, +} + +impl From for Config { + fn from(c: Config458) -> Self { + Self { + bat_charge_limit: c.bat_charge_limit, + panel_od: c.panel_od, + disable_nvidia_powerd_on_battery: true, + ac_command: c.ac_command, + bat_command: c.bat_command, + } + } +} diff --git a/asusd/src/ctrl_anime/config.rs b/asusd/src/ctrl_anime/config.rs new file mode 100644 index 00000000..72c77783 --- /dev/null +++ b/asusd/src/ctrl_anime/config.rs @@ -0,0 +1,214 @@ +use std::time::Duration; + +use config_traits::{StdConfig, StdConfigLoad2}; +use rog_anime::error::AnimeError; +use rog_anime::usb::Brightness; +use rog_anime::{ActionData, ActionLoader, AnimTime, Animations, AnimeType, Fade, Vec2}; +use serde_derive::{Deserialize, Serialize}; + +const CONFIG_FILE: &str = "anime.ron"; + +#[derive(Deserialize, Serialize)] +pub struct AnimeConfigV460 { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub sleep: Vec, + pub shutdown: Vec, + pub brightness: f32, +} + +impl From for AnimeConfig { + fn from(c: AnimeConfigV460) -> AnimeConfig { + AnimeConfig { + system: c.system, + boot: c.boot, + wake: c.wake, + sleep: c.sleep, + shutdown: c.shutdown, + ..Default::default() + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct AnimeConfigV5 { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub sleep: Vec, + pub shutdown: Vec, + pub brightness: f32, + pub awake_enabled: bool, + pub boot_anim_enabled: bool, +} + +impl From for AnimeConfig { + fn from(c: AnimeConfigV5) -> AnimeConfig { + AnimeConfig { + system: c.system, + boot: c.boot, + wake: c.wake, + sleep: c.sleep, + shutdown: c.shutdown, + ..Default::default() + } + } +} + +#[derive(Deserialize, Serialize, Default)] +pub struct AnimeConfigCached { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub sleep: Vec, + pub shutdown: Vec, +} + +impl AnimeConfigCached { + pub fn init_from_config( + &mut self, + config: &AnimeConfig, + anime_type: AnimeType, + ) -> Result<(), AnimeError> { + let mut sys = Vec::with_capacity(config.system.len()); + for ani in &config.system { + sys.push(ActionData::from_anime_action(anime_type, ani)?); + } + self.system = sys; + + let mut boot = Vec::with_capacity(config.boot.len()); + for ani in &config.boot { + boot.push(ActionData::from_anime_action(anime_type, ani)?); + } + self.boot = boot; + + let mut wake = Vec::with_capacity(config.wake.len()); + for ani in &config.wake { + wake.push(ActionData::from_anime_action(anime_type, ani)?); + } + self.wake = wake; + + let mut sleep = Vec::with_capacity(config.sleep.len()); + for ani in &config.sleep { + sleep.push(ActionData::from_anime_action(anime_type, ani)?); + } + self.sleep = sleep; + + let mut shutdown = Vec::with_capacity(config.shutdown.len()); + for ani in &config.shutdown { + shutdown.push(ActionData::from_anime_action(anime_type, ani)?); + } + self.shutdown = shutdown; + Ok(()) + } +} + +/// Config for base system actions for the anime display +#[derive(Deserialize, Serialize, Debug)] +pub struct AnimeConfig { + pub system: Vec, + pub boot: Vec, + pub wake: Vec, + pub sleep: Vec, + pub shutdown: Vec, + pub brightness: f32, + pub display_enabled: bool, + pub display_brightness: Brightness, + pub builtin_anims_enabled: bool, + pub builtin_anims: Animations, +} + +impl Default for AnimeConfig { + fn default() -> Self { + AnimeConfig { + system: Vec::new(), + boot: Vec::new(), + wake: Vec::new(), + sleep: Vec::new(), + shutdown: Vec::new(), + brightness: 1.0, + display_enabled: true, + display_brightness: Brightness::Med, + builtin_anims_enabled: true, + builtin_anims: Animations::default(), + } + } +} + +impl StdConfig for AnimeConfig { + fn new() -> Self { + Self::create_default() + } + + fn config_dir() -> std::path::PathBuf { + std::path::PathBuf::from(crate::CONFIG_PATH_BASE) + } + + fn file_name(&self) -> String { + CONFIG_FILE.to_owned() + } +} + +impl StdConfigLoad2 for AnimeConfig {} + +impl AnimeConfig { + // fn clamp_config_brightness(mut config: &mut AnimeConfig) { + // if config.brightness < 0.0 || config.brightness > 1.0 { + // warn!( + // "Clamped brightness to [0.0 ; 1.0], was {}", + // config.brightness + // ); + // config.brightness = f32::max(0.0, f32::min(1.0, config.brightness)); + // } + // } + + fn create_default() -> Self { + // create a default config here + 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), + )), + }], + sleep: 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, + }], + 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, + ..Default::default() + } + } +} diff --git a/asusd/src/ctrl_anime/mod.rs b/asusd/src/ctrl_anime/mod.rs new file mode 100644 index 00000000..dd02f53e --- /dev/null +++ b/asusd/src/ctrl_anime/mod.rs @@ -0,0 +1,261 @@ +pub mod config; +/// Implements `CtrlTask`, Reloadable, `ZbusRun` +pub mod trait_impls; + +use std::convert::TryFrom; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::sleep; + +use ::zbus::export::futures_util::lock::Mutex; +use log::{error, info, warn}; +use rog_anime::error::AnimeError; +use rog_anime::usb::{get_anime_type, pkt_flush, pkt_set_enable_powersave_anim, pkts_for_init}; +use rog_anime::{ActionData, AnimeDataBuffer, AnimePacketType, AnimeType}; +use rog_platform::hid_raw::HidRaw; +use rog_platform::supported::AnimeSupportedFunctions; +use rog_platform::usb_raw::USBRaw; + +use self::config::{AnimeConfig, AnimeConfigCached}; +use crate::error::RogError; +use crate::GetSupported; + +impl GetSupported for CtrlAnime { + type A = AnimeSupportedFunctions; + + fn get_supported() -> Self::A { + AnimeSupportedFunctions(HidRaw::new("193b").is_ok()) + } +} + +enum Node { + Usb(USBRaw), + Hid(HidRaw), +} + +impl Node { + pub fn write_bytes(&self, message: &[u8]) -> Result<(), RogError> { + // TODO: map and pass on errors + match self { + Node::Usb(u) => { + u.write_bytes(message).ok(); + } + Node::Hid(h) => { + h.write_bytes(message).ok(); + } + } + Ok(()) + } +} + +pub struct CtrlAnime { + // node: HidRaw, + node: Node, + anime_type: AnimeType, + 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 { + // let node = HidRaw::new("193b")?; + let usb = USBRaw::new(0x193b).ok(); + let hid = HidRaw::new("193b").ok(); + let node = if usb.is_some() { + unsafe { Node::Usb(usb.unwrap_unchecked()) } + } else if hid.is_some() { + unsafe { Node::Hid(hid.unwrap_unchecked()) } + } else { + return Err(RogError::Anime(AnimeError::NoDevice)); + }; + + let anime_type = get_anime_type().unwrap_or(AnimeType::GA402); + + info!("Device has an AniMe Matrix display: {anime_type:?}"); + let mut cache = AnimeConfigCached::default(); + cache.init_from_config(&config, anime_type)?; + + let ctrl = CtrlAnime { + node, + anime_type, + cache, + config, + thread_exit: Arc::new(AtomicBool::new(false)), + thread_running: Arc::new(AtomicBool::new(false)), + }; + ctrl.do_initialization()?; + + Ok(ctrl) + } + + // let device = CtrlAnime::get_device(0x0b05, 0x193b)?; + + /// 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. + async fn run_thread(inner: Arc>, actions: Vec, mut once: bool) { + if actions.is_empty() { + warn!("AniMe system actions was empty"); + return; + } + + if let Some(lock) = inner.try_lock() { + lock.node + .write_bytes(&pkt_set_enable_powersave_anim(false)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + } + + // 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 + // TODO: turn this in to async task (maybe? COuld still risk blocking main + // thread) + std::thread::Builder::new() + .name("AniMe system thread start".into()) + .spawn(move || { + info!("AniMe new 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; + let anime_type; + loop { + if let Some(lock) = inner.try_lock() { + thread_exit = lock.thread_exit.clone(); + thread_running = lock.thread_running.clone(); + anime_type = lock.anime_type; + break; + } + } + // 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. + while thread_running.load(Ordering::SeqCst) { + // Make any running loop exit first + thread_exit.store(true, Ordering::SeqCst); + } + + info!("AniMe no previous system thread running (now)"); + thread_exit.store(false, Ordering::SeqCst); + thread_running.store(true, Ordering::SeqCst); + 'main: loop { + for action in &actions { + if thread_exit.load(Ordering::SeqCst) { + break 'main; + } + match action { + ActionData::Animation(frames) => { + rog_anime::run_animation(frames, &|frame| { + if thread_exit.load(Ordering::Acquire) { + info!("rog-anime: animation sub-loop was asked to exit"); + return Ok(true); // Do safe exit + } + inner + .try_lock() + .map(|lock| { + lock.write_data_buffer(frame) + .map_err(|err| { + warn!( + "rog_anime::run_animation:callback {}", + err + ); + }) + .ok(); + false // Don't exit yet + }) + .map_or_else( + || { + warn!("rog_anime::run_animation:callback failed"); + Err(AnimeError::NoFrames) + }, + Ok, + ) + }); + if thread_exit.load(Ordering::Acquire) { + info!("rog-anime: sub-loop exited and main loop exiting now"); + break 'main; + } + } + ActionData::Image(image) => { + once = false; + if let Some(lock) = inner.try_lock() { + lock.write_data_buffer(image.as_ref().clone()) + .map_err(|e| error!("{}", e)) + .ok(); + } + } + ActionData::Pause(duration) => sleep(*duration), + ActionData::AudioEq + | ActionData::SystemInfo + | ActionData::TimeDate + | ActionData::Matrix => {} + } + } + if thread_exit.load(Ordering::SeqCst) { + break 'main; + } + if once || actions.is_empty() { + break 'main; + } + } + // Clear the display on exit + if let Some(lock) = inner.try_lock() { + if let Ok(data) = + AnimeDataBuffer::from_vec(anime_type, vec![0u8; anime_type.data_length()]) + .map_err(|e| error!("{}", e)) + { + lock.write_data_buffer(data) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + } + } + // Loop ended, set the atmonics + thread_running.store(false, Ordering::SeqCst); + info!("AniMe system thread exited"); + }) + .map(|err| info!("AniMe system thread: {:?}", err)) + .ok(); + } + + /// 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) -> Result<(), RogError> { + for led in buffer.data_mut().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::try_from(buffer)?; + for row in &data { + self.node.write_bytes(row)?; + } + self.node.write_bytes(&pkt_flush())?; + Ok(()) + } + + fn do_initialization(&self) -> Result<(), RogError> { + let pkts = pkts_for_init(); + self.node.write_bytes(&pkts[0])?; + self.node.write_bytes(&pkts[1])?; + Ok(()) + } +} diff --git a/asusd/src/ctrl_anime/trait_impls.rs b/asusd/src/ctrl_anime/trait_impls.rs new file mode 100644 index 00000000..b52d6e30 --- /dev/null +++ b/asusd/src/ctrl_anime/trait_impls.rs @@ -0,0 +1,299 @@ +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use async_trait::async_trait; +use config_traits::StdConfig; +use log::warn; +use rog_anime::usb::{ + pkt_set_brightness, pkt_set_builtin_animations, pkt_set_enable_display, + pkt_set_enable_powersave_anim, AnimAwake, AnimBooting, AnimShutdown, AnimSleeping, Brightness, +}; +use rog_anime::{AnimeDataBuffer, DeviceState}; +use zbus::export::futures_util::lock::Mutex; +use zbus::{dbus_interface, Connection, SignalContext}; + +use super::CtrlAnime; +use crate::error::RogError; + +pub(super) const ZBUS_PATH: &str = "/org/asuslinux/Anime"; + +#[derive(Clone)] +pub struct CtrlAnimeZbus(pub Arc>); + +/// The struct with the main dbus methods requires this trait +#[async_trait] +impl crate::ZbusRun for CtrlAnimeZbus { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, ZBUS_PATH, server).await; + } +} + +// 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 + async fn write(&self, input: AnimeDataBuffer) -> zbus::fdo::Result<()> { + let lock = self.0.lock().await; + lock.thread_exit.store(true, Ordering::SeqCst); + lock.write_data_buffer(input).map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + err + })?; + Ok(()) + } + + /// Set the global AniMe brightness + async fn set_image_brightness(&self, bright: f32) { + let mut lock = self.0.lock().await; + let mut bright = bright; + if bright < 0.0 { + bright = 0.0; + } else if bright > 1.0 { + bright = 1.0; + } + lock.config.brightness = bright; + lock.config.write(); + } + + /// Set base brightness level + // TODO: enum for brightness + async fn set_brightness( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + brightness: Brightness, + ) { + let mut lock = self.0.lock().await; + lock.node + .write_bytes(&pkt_set_brightness(brightness)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + lock.config.display_brightness = brightness; + lock.config.write(); + + Self::notify_device_state( + &ctxt, + DeviceState { + display_enabled: lock.config.display_enabled, + display_brightness: lock.config.display_brightness, + builtin_anims_enabled: lock.config.builtin_anims_enabled, + builtin_anims: lock.config.builtin_anims, + }, + ) + .await + .ok(); + } + + /// Enable the builtin animations or not + async fn set_builtins_enabled( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + enabled: bool, + ) { + let mut lock = self.0.lock().await; + lock.node + .write_bytes(&pkt_set_enable_powersave_anim(enabled)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + lock.config.builtin_anims_enabled = enabled; + lock.config.write(); + if enabled { + lock.thread_exit.store(true, Ordering::Release); + } + + Self::notify_device_state( + &ctxt, + DeviceState { + display_enabled: lock.config.display_enabled, + display_brightness: lock.config.display_brightness, + builtin_anims_enabled: lock.config.builtin_anims_enabled, + builtin_anims: lock.config.builtin_anims, + }, + ) + .await + .ok(); + } + + /// Set which builtin animation is used for each stage + async fn set_builtin_animations( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + boot: AnimBooting, + awake: AnimAwake, + sleep: AnimSleeping, + shutdown: AnimShutdown, + ) { + let mut lock = self.0.lock().await; + lock.node + .write_bytes(&pkt_set_enable_powersave_anim(true)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + lock.node + .write_bytes(&pkt_set_builtin_animations(boot, awake, sleep, shutdown)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + lock.config.builtin_anims.boot = boot; + lock.config.builtin_anims.sleep = sleep; + lock.config.builtin_anims.awake = awake; + lock.config.builtin_anims.shutdown = shutdown; + lock.config.write(); + + Self::notify_device_state( + &ctxt, + DeviceState { + display_enabled: lock.config.display_enabled, + display_brightness: lock.config.display_brightness, + builtin_anims_enabled: lock.config.builtin_anims_enabled, + builtin_anims: lock.config.builtin_anims, + }, + ) + .await + .ok(); + } + + /// Set whether the AniMe is enabled at all + async fn set_enable_display( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + enabled: bool, + ) { + let mut lock = self.0.lock().await; + lock.node + .write_bytes(&pkt_set_enable_display(enabled)) + .map_err(|err| { + warn!("rog_anime::run_animation:callback {}", err); + }) + .ok(); + lock.config.display_enabled = enabled; + lock.config.write(); + + Self::notify_device_state( + &ctxt, + DeviceState { + display_enabled: lock.config.display_enabled, + display_brightness: lock.config.display_brightness, + builtin_anims_enabled: lock.config.builtin_anims_enabled, + builtin_anims: lock.config.builtin_anims, + }, + ) + .await + .ok(); + } + + /// The main loop is the base system set action if the user isn't running + /// the user daemon + async fn run_main_loop(&self, start: bool) { + if start { + let lock = self.0.lock().await; + lock.thread_exit.store(true, Ordering::SeqCst); + CtrlAnime::run_thread(self.0.clone(), lock.cache.system.clone(), false).await; + } + } + + /// Get the device state as stored by asusd + // #[dbus_interface(property)] + async fn device_state(&self) -> DeviceState { + let lock = self.0.lock().await; + DeviceState { + display_enabled: lock.config.display_enabled, + display_brightness: lock.config.display_brightness, + builtin_anims_enabled: lock.config.builtin_anims_enabled, + builtin_anims: lock.config.builtin_anims, + } + } + + /// Notify listeners of the status of AniMe LED power and factory + /// system-status animations + #[dbus_interface(signal)] + async fn notify_device_state(ctxt: &SignalContext<'_>, data: DeviceState) -> zbus::Result<()>; +} + +#[async_trait] +impl crate::CtrlTask for CtrlAnimeZbus { + fn zbus_path() -> &'static str { + ZBUS_PATH + } + + async fn create_tasks(&self, _: SignalContext<'static>) -> Result<(), RogError> { + let inner1 = self.0.clone(); + let inner2 = self.0.clone(); + let inner3 = self.0.clone(); + let inner4 = self.0.clone(); + self.create_sys_event_tasks( + move || { + // on_sleep + let inner1 = inner1.clone(); + async move { + let lock = inner1.lock().await; + CtrlAnime::run_thread(inner1.clone(), lock.cache.sleep.clone(), true).await; + } + }, + move || { + // on_wake + let inner2 = inner2.clone(); + async move { + let lock = inner2.lock().await; + CtrlAnime::run_thread(inner2.clone(), lock.cache.wake.clone(), true).await; + } + }, + move || { + // on_shutdown + let inner3 = inner3.clone(); + async move { + let lock = inner3.lock().await; + CtrlAnime::run_thread(inner3.clone(), lock.cache.shutdown.clone(), true).await; + } + }, + move || { + // on_boot + let inner4 = inner4.clone(); + async move { + let lock = inner4.lock().await; + CtrlAnime::run_thread(inner4.clone(), lock.cache.boot.clone(), true).await; + } + }, + ) + .await; + + Ok(()) + } +} + +#[async_trait] +impl crate::Reloadable for CtrlAnimeZbus { + async fn reload(&mut self) -> Result<(), RogError> { + if let Some(lock) = self.0.try_lock() { + let anim = &lock.config.builtin_anims; + lock.node + .write_bytes(&pkt_set_enable_display(lock.config.display_enabled))?; + lock.node.write_bytes(&pkt_set_enable_powersave_anim( + lock.config.builtin_anims_enabled, + ))?; + lock.node.write_bytes(&pkt_set_builtin_animations( + anim.boot, + anim.awake, + anim.sleep, + anim.shutdown, + ))?; + + if lock.config.builtin_anims_enabled && !lock.cache.boot.is_empty() { + lock.node + .write_bytes(&pkt_set_enable_powersave_anim(false)) + .ok(); + } + let action = lock.cache.boot.clone(); + CtrlAnime::run_thread(self.0.clone(), action, true).await; + } + Ok(()) + } +} diff --git a/asusd/src/ctrl_aura/config.rs b/asusd/src/ctrl_aura/config.rs new file mode 100644 index 00000000..566c9e39 --- /dev/null +++ b/asusd/src/ctrl_aura/config.rs @@ -0,0 +1,357 @@ +use std::collections::{BTreeMap, HashSet}; + +use config_traits::{StdConfig, StdConfigLoad}; +use rog_aura::aura_detection::LaptopLedData; +use rog_aura::usb::{AuraDevRog1, AuraDevRog2, AuraDevTuf, AuraDevice, AuraPowerDev}; +use rog_aura::{AuraEffect, AuraModeNum, AuraZone, Direction, LedBrightness, Speed, GRADIENT}; +use serde_derive::{Deserialize, Serialize}; + +const CONFIG_FILE: &str = "aura.ron"; + +/// Enable/disable LED control in various states such as +/// when the device is awake, suspended, shutting down or +/// booting. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum AuraPowerConfig { + AuraDevTuf(HashSet), + AuraDevRog1(HashSet), + AuraDevRog2(HashSet), +} + +impl AuraPowerConfig { + /// Invalid for TUF laptops + pub fn to_bytes(control: &Self) -> [u8; 4] { + match control { + AuraPowerConfig::AuraDevTuf(_) => [0, 0, 0, 0], + AuraPowerConfig::AuraDevRog1(c) => { + let c: Vec = c.iter().copied().collect(); + AuraDevRog1::to_bytes(&c) + } + AuraPowerConfig::AuraDevRog2(c) => { + let c: Vec = c.iter().copied().collect(); + AuraDevRog2::to_bytes(&c) + } + } + } + + pub fn to_tuf_bool_array(control: &Self) -> Option<[bool; 5]> { + if let Self::AuraDevTuf(c) = control { + return Some([ + true, + c.contains(&AuraDevTuf::Boot), + c.contains(&AuraDevTuf::Awake), + c.contains(&AuraDevTuf::Sleep), + c.contains(&AuraDevTuf::Keyboard), + ]); + } + + if let Self::AuraDevRog1(c) = control { + return Some([ + true, + c.contains(&AuraDevRog1::Boot), + c.contains(&AuraDevRog1::Awake), + c.contains(&AuraDevRog1::Sleep), + c.contains(&AuraDevRog1::Keyboard), + ]); + } + + None + } + + pub fn set_tuf(&mut self, power: AuraDevTuf, on: bool) { + if let Self::AuraDevTuf(p) = self { + if on { + p.insert(power); + } else { + p.remove(&power); + } + } + } + + pub fn set_0x1866(&mut self, power: AuraDevRog1, on: bool) { + if let Self::AuraDevRog1(p) = self { + if on { + p.insert(power); + } else { + p.remove(&power); + } + } + } + + pub fn set_0x19b6(&mut self, power: AuraDevRog2, on: bool) { + if let Self::AuraDevRog2(p) = self { + if on { + p.insert(power); + } else { + p.remove(&power); + } + } + } +} + +impl From<&AuraPowerConfig> for AuraPowerDev { + fn from(config: &AuraPowerConfig) -> Self { + match config { + AuraPowerConfig::AuraDevTuf(d) => AuraPowerDev { + tuf: d.iter().copied().collect(), + x1866: vec![], + x19b6: vec![], + }, + AuraPowerConfig::AuraDevRog1(d) => AuraPowerDev { + tuf: vec![], + x1866: d.iter().copied().collect(), + x19b6: vec![], + }, + AuraPowerConfig::AuraDevRog2(d) => AuraPowerDev { + tuf: vec![], + x1866: vec![], + x19b6: d.iter().copied().collect(), + }, + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +// #[serde(default)] +pub struct AuraConfig { + pub brightness: LedBrightness, + pub current_mode: AuraModeNum, + pub builtins: BTreeMap, + pub multizone: Option>>, + pub multizone_on: bool, + pub enabled: AuraPowerConfig, +} + +impl StdConfig for AuraConfig { + fn new() -> Self { + // Self::create_default(AuraDevice::X19b6, &LaptopLedData::get_data()) + panic!("AuraConfig::new() should not be used, use AuraConfig::create_default() instead"); + } + + fn config_dir() -> std::path::PathBuf { + std::path::PathBuf::from(crate::CONFIG_PATH_BASE) + } + + fn file_name(&self) -> String { + CONFIG_FILE.to_owned() + } +} + +impl StdConfigLoad for AuraConfig {} + +impl AuraConfig { + pub fn create_default(prod_id: AuraDevice, support_data: &LaptopLedData) -> Self { + // create a default config here + let enabled = if prod_id == AuraDevice::X19b6 { + AuraPowerConfig::AuraDevRog2(HashSet::from([ + AuraDevRog2::BootLogo, + AuraDevRog2::BootKeyb, + AuraDevRog2::SleepLogo, + AuraDevRog2::SleepKeyb, + AuraDevRog2::AwakeLogo, + AuraDevRog2::AwakeKeyb, + AuraDevRog2::ShutdownLogo, + AuraDevRog2::ShutdownKeyb, + AuraDevRog2::BootBar, + AuraDevRog2::AwakeBar, + AuraDevRog2::SleepBar, + AuraDevRog2::ShutdownBar, + AuraDevRog2::BootRearGlow, + AuraDevRog2::AwakeRearGlow, + AuraDevRog2::SleepRearGlow, + AuraDevRog2::ShutdownRearGlow, + ])) + } else if prod_id == AuraDevice::Tuf { + AuraPowerConfig::AuraDevTuf(HashSet::from([ + AuraDevTuf::Awake, + AuraDevTuf::Boot, + AuraDevTuf::Sleep, + AuraDevTuf::Keyboard, + ])) + } else { + AuraPowerConfig::AuraDevRog1(HashSet::from([ + AuraDevRog1::Awake, + AuraDevRog1::Boot, + AuraDevRog1::Sleep, + AuraDevRog1::Keyboard, + AuraDevRog1::Lightbar, + ])) + }; + let mut config = AuraConfig { + brightness: LedBrightness::Med, + current_mode: AuraModeNum::Static, + builtins: BTreeMap::new(), + multizone: None, + multizone_on: false, + enabled, + }; + + for n in &support_data.basic_modes { + config + .builtins + .insert(*n, AuraEffect::default_with_mode(*n)); + + if !support_data.basic_zones.is_empty() { + let mut default = vec![]; + for (i, tmp) in support_data.basic_zones.iter().enumerate() { + default.push(AuraEffect { + mode: *n, + zone: *tmp, + colour1: *GRADIENT.get(i).unwrap_or(&GRADIENT[0]), + colour2: *GRADIENT.get(GRADIENT.len() - i).unwrap_or(&GRADIENT[6]), + speed: Speed::Med, + direction: Direction::Left, + }); + } + if let Some(m) = config.multizone.as_mut() { + m.insert(*n, default); + } else { + let mut tmp = BTreeMap::new(); + tmp.insert(*n, default); + config.multizone = Some(tmp); + } + } + } + config + } + + /// Set the mode data, current mode, and if multizone enabled. + /// + /// Multipurpose, will accept `AuraEffect` with zones and put in the correct + /// store. + pub fn set_builtin(&mut self, effect: AuraEffect) { + self.current_mode = effect.mode; + if effect.zone() == AuraZone::None { + self.builtins.insert(*effect.mode(), effect); + self.multizone_on = false; + } else { + if let Some(multi) = self.multizone.as_mut() { + if let Some(fx) = multi.get_mut(effect.mode()) { + for fx in fx.iter_mut() { + if fx.zone == effect.zone { + *fx = effect; + return; + } + } + fx.push(effect); + } else { + multi.insert(*effect.mode(), vec![effect]); + } + } else { + let mut tmp = BTreeMap::new(); + tmp.insert(*effect.mode(), vec![effect]); + self.multizone = Some(tmp); + } + self.multizone_on = true; + } + } + + pub fn get_multizone(&self, aura_type: AuraModeNum) -> Option<&[AuraEffect]> { + if let Some(multi) = &self.multizone { + return multi.get(&aura_type).map(|v| v.as_slice()); + } + None + } +} + +#[cfg(test)] +mod tests { + use rog_aura::aura_detection::LaptopLedData; + use rog_aura::usb::AuraDevice; + use rog_aura::{AuraEffect, AuraModeNum, AuraZone, Colour}; + + use super::AuraConfig; + + #[test] + fn set_multizone_4key_config() { + let mut config = AuraConfig::create_default(AuraDevice::X19b6, &LaptopLedData::default()); + + let effect = AuraEffect { + colour1: Colour(0xff, 0x00, 0xff), + zone: AuraZone::Key1, + ..Default::default() + }; + config.set_builtin(effect); + + assert!(config.multizone.is_some()); + + let effect = AuraEffect { + colour1: Colour(0x00, 0xff, 0xff), + zone: AuraZone::Key2, + ..Default::default() + }; + config.set_builtin(effect); + + let effect = AuraEffect { + colour1: Colour(0xff, 0xff, 0x00), + zone: AuraZone::Key3, + ..Default::default() + }; + config.set_builtin(effect); + + let effect = AuraEffect { + colour1: Colour(0x00, 0xff, 0x00), + zone: AuraZone::Key4, + ..Default::default() + }; + let effect_clone = effect.clone(); + config.set_builtin(effect); + // This should replace existing + config.set_builtin(effect_clone); + + let res = config.multizone.unwrap(); + let sta = res.get(&AuraModeNum::Static).unwrap(); + assert_eq!(sta.len(), 4); + assert_eq!(sta[0].colour1, Colour(0xff, 0x00, 0xff)); + assert_eq!(sta[1].colour1, Colour(0x00, 0xff, 0xff)); + assert_eq!(sta[2].colour1, Colour(0xff, 0xff, 0x00)); + assert_eq!(sta[3].colour1, Colour(0x00, 0xff, 0x00)); + } + + #[test] + fn set_multizone_multimode_config() { + let mut config = AuraConfig::create_default(AuraDevice::X19b6, &LaptopLedData::default()); + + let effect = AuraEffect { + zone: AuraZone::Key1, + ..Default::default() + }; + config.set_builtin(effect); + + assert!(config.multizone.is_some()); + + let effect = AuraEffect { + zone: AuraZone::Key2, + mode: AuraModeNum::Breathe, + ..Default::default() + }; + config.set_builtin(effect); + + let effect = AuraEffect { + zone: AuraZone::Key3, + mode: AuraModeNum::Comet, + ..Default::default() + }; + config.set_builtin(effect); + + let effect = AuraEffect { + zone: AuraZone::Key4, + mode: AuraModeNum::Pulse, + ..Default::default() + }; + config.set_builtin(effect); + + let res = config.multizone.unwrap(); + let sta = res.get(&AuraModeNum::Static).unwrap(); + assert_eq!(sta.len(), 1); + + let sta = res.get(&AuraModeNum::Breathe).unwrap(); + assert_eq!(sta.len(), 1); + + let sta = res.get(&AuraModeNum::Comet).unwrap(); + assert_eq!(sta.len(), 1); + + let sta = res.get(&AuraModeNum::Pulse).unwrap(); + assert_eq!(sta.len(), 1); + } +} diff --git a/asusd/src/ctrl_aura/controller.rs b/asusd/src/ctrl_aura/controller.rs new file mode 100644 index 00000000..041b8e61 --- /dev/null +++ b/asusd/src/ctrl_aura/controller.rs @@ -0,0 +1,558 @@ +use std::collections::BTreeMap; + +use config_traits::{StdConfig, StdConfigLoad}; +use log::{info, warn}; +use rog_aura::advanced::{LedUsbPackets, UsbPackets}; +use rog_aura::aura_detection::{LaptopLedData, ASUS_KEYBOARD_DEVICES}; +use rog_aura::usb::{AuraDevice, LED_APPLY, LED_SET}; +use rog_aura::{AuraEffect, AuraZone, Direction, LedBrightness, Speed, GRADIENT, LED_MSG_LEN}; +use rog_platform::hid_raw::HidRaw; +use rog_platform::keyboard_led::KeyboardLed; +use rog_platform::supported::LedSupportedFunctions; + +use super::config::{AuraConfig, AuraPowerConfig}; +use crate::error::RogError; +use crate::GetSupported; + +impl GetSupported for CtrlKbdLed { + type A = LedSupportedFunctions; + + fn get_supported() -> Self::A { + // let mode = <&str>::from(&::from(*mode)); + let laptop = LaptopLedData::get_data(); + let stock_led_modes = laptop.basic_modes; + let multizone_led_mode = laptop.basic_zones; + let advanced_type = laptop.advanced_type; + + let mut prod_id = AuraDevice::Unknown; + for prod in ASUS_KEYBOARD_DEVICES { + if HidRaw::new(prod.into()).is_ok() { + prod_id = prod; + break; + } + } + + let rgb = KeyboardLed::new(); + if let Ok(p) = rgb.as_ref() { + if p.has_kbd_rgb_mode() { + prod_id = AuraDevice::Tuf; + } + } + + LedSupportedFunctions { + dev_id: prod_id, + brightness: rgb.is_ok(), + basic_modes: stock_led_modes, + basic_zones: multizone_led_mode, + advanced_type: advanced_type.into(), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd)] +pub enum LEDNode { + KbdLed(KeyboardLed), + Rog(HidRaw), + None, +} + +pub struct CtrlKbdLed { + // TODO: config stores the keyboard type as an AuraPower, use or update this + pub led_prod: AuraDevice, + pub led_node: LEDNode, + pub kd_brightness: KeyboardLed, + pub supported_modes: LaptopLedData, + pub flip_effect_write: bool, + pub per_key_mode_active: bool, + pub config: AuraConfig, +} + +impl CtrlKbdLed { + pub fn new(supported_modes: LaptopLedData) -> Result { + let mut led_prod = AuraDevice::Unknown; + let mut usb_node = None; + for prod in ASUS_KEYBOARD_DEVICES { + match HidRaw::new(prod.into()) { + Ok(node) => { + led_prod = prod; + usb_node = Some(node); + info!( + "Looked for keyboard controller 0x{}: Found", + <&str>::from(prod) + ); + break; + } + Err(err) => info!( + "Looked for keyboard controller 0x{}: {err}", + <&str>::from(prod) + ), + } + } + + let rgb_led = KeyboardLed::new()?; + + if usb_node.is_none() && !rgb_led.has_kbd_rgb_mode() { + let dmi = sysfs_class::DmiId::default(); + if let Ok(prod_family) = dmi.product_family() { + if prod_family.contains("TUF") { + warn!( + "kbd_rgb_mode was not found in the /sys/. You require a minimum 6.1 \ + kernel and a supported TUF laptop" + ); + } + } + return Err(RogError::NoAuraKeyboard); + } + + let led_node = if let Some(rog) = usb_node { + info!("Found ROG USB keyboard"); + LEDNode::Rog(rog) + } else if rgb_led.has_kbd_rgb_mode() { + info!("Found TUF keyboard"); + LEDNode::KbdLed(rgb_led.clone()) + } else { + LEDNode::None + }; + + let mut config_init = AuraConfig::create_default(led_prod, &supported_modes); + let mut config_loaded = config_init.clone().load(); + + for mode in &mut config_init.builtins { + // update init values from loaded values if they exist + if let Some(loaded) = config_loaded.builtins.get(mode.0) { + *mode.1 = loaded.clone(); + } + } + config_loaded.builtins = config_init.builtins; + + if let (Some(mut multizone_init), Some(multizone_loaded)) = + (config_init.multizone, config_loaded.multizone.as_mut()) + { + for mode in multizone_init.iter_mut() { + // update init values from loaded values if they exist + if let Some(loaded) = multizone_loaded.get(mode.0) { + let mut new_set = Vec::new(); + // only reuse a zone mode if the mode is supported + for mode in loaded { + if supported_modes.basic_modes.contains(&mode.mode) { + new_set.push(mode.clone()); + } + } + *mode.1 = new_set; + } + } + *multizone_loaded = multizone_init; + } + + let ctrl = CtrlKbdLed { + led_prod, + led_node, // on TUF this is the same as rgb_led / kd_brightness + kd_brightness: rgb_led, // If was none then we already returned above + supported_modes, + flip_effect_write: false, + per_key_mode_active: false, + config: config_loaded, + }; + Ok(ctrl) + } + + pub(super) fn get_brightness(&self) -> Result { + self.kd_brightness + .get_brightness() + .map_err(RogError::Platform) + } + + pub(super) fn set_brightness(&self, brightness: LedBrightness) -> Result<(), RogError> { + self.kd_brightness + .set_brightness(brightness as u8) + .map_err(RogError::Platform) + } + + pub fn next_brightness(&mut self) -> Result<(), RogError> { + let mut bright = (self.config.brightness as u32) + 1; + if bright > 3 { + bright = 0; + } + self.config.brightness = ::from(bright); + self.config.write(); + self.set_brightness(self.config.brightness) + } + + pub fn prev_brightness(&mut self) -> Result<(), RogError> { + let mut bright = self.config.brightness as u32; + if bright == 0 { + bright = 3; + } else { + bright -= 1; + } + self.config.brightness = ::from(bright); + self.config.write(); + self.set_brightness(self.config.brightness) + } + + /// Set combination state for boot animation/sleep animation/all leds/keys + /// leds/side leds LED active + pub(super) fn set_power_states(&mut self) -> Result<(), RogError> { + if let LEDNode::KbdLed(platform) = &mut self.led_node { + if let Some(pwr) = AuraPowerConfig::to_tuf_bool_array(&self.config.enabled) { + let buf = [1, pwr[1] as u8, pwr[2] as u8, pwr[3] as u8, pwr[4] as u8]; + platform.set_kbd_rgb_state(&buf)?; + } + } else if let LEDNode::Rog(hid_raw) = &self.led_node { + let bytes = AuraPowerConfig::to_bytes(&self.config.enabled); + let message = [0x5d, 0xbd, 0x01, bytes[0], bytes[1], bytes[2], bytes[3]]; + + hid_raw.write_bytes(&message)?; + hid_raw.write_bytes(&LED_SET)?; + // Changes won't persist unless apply is set + hid_raw.write_bytes(&LED_APPLY)?; + } + Ok(()) + } + + /// Set an Aura effect if the effect mode or zone is supported. + /// + /// On success the aura config file is read to refresh cached values, then + /// the effect is stored and config written to disk. + pub(crate) fn set_effect(&mut self, effect: AuraEffect) -> Result<(), RogError> { + if !self.supported_modes.basic_modes.contains(&effect.mode) + || effect.zone != AuraZone::None + && !self.supported_modes.basic_zones.contains(&effect.zone) + { + return Err(RogError::AuraEffectNotSupported); + } + + self.write_mode(&effect)?; + self.config.read(); // refresh config if successful + self.config.set_builtin(effect); + if self.config.brightness == LedBrightness::Off { + self.config.brightness = LedBrightness::Med; + } + self.config.write(); + self.set_brightness(self.config.brightness)?; + Ok(()) + } + + /// Write an effect block. This is for per-key, but can be repurposed to + /// write the raw factory mode packets - when doing this it is expected that + /// only the first `Vec` (`effect[0]`) is valid. + pub fn write_effect_block(&mut self, effect: &UsbPackets) -> Result<(), RogError> { + if self.config.brightness == LedBrightness::Off { + self.config.brightness = LedBrightness::Med; + self.config.write(); + } + + let pkt_type = effect[0][1]; + const PER_KEY_TYPE: u8 = 0xbc; + + if pkt_type != PER_KEY_TYPE { + self.per_key_mode_active = false; + if let LEDNode::Rog(hid_raw) = &self.led_node { + hid_raw.write_bytes(&effect[0])?; + hid_raw.write_bytes(&LED_SET)?; + // hid_raw.write_bytes(&LED_APPLY)?; + } + } else { + if !self.per_key_mode_active { + if let LEDNode::Rog(hid_raw) = &self.led_node { + let init = LedUsbPackets::get_init_msg(); + hid_raw.write_bytes(&init)?; + } + self.per_key_mode_active = true; + } + if let LEDNode::Rog(hid_raw) = &self.led_node { + for row in effect.iter() { + hid_raw.write_bytes(row)?; + } + } else if let LEDNode::KbdLed(tuf) = &self.led_node { + for row in effect.iter() { + let r = row[9]; + let g = row[10]; + let b = row[11]; + tuf.set_kbd_rgb_mode(&[0, 0, r, g, b, 0])?; + } + } + self.flip_effect_write = !self.flip_effect_write; + } + Ok(()) + } + + pub(super) fn toggle_mode(&mut self, reverse: bool) -> Result<(), RogError> { + let current = self.config.current_mode; + if let Some(idx) = self + .supported_modes + .basic_modes + .iter() + .position(|v| *v == current) + { + let mut idx = idx; + // goes past end of array + if reverse { + if idx == 0 { + idx = self.supported_modes.basic_modes.len() - 1; + } else { + idx -= 1; + } + } else { + idx += 1; + if idx == self.supported_modes.basic_modes.len() { + idx = 0; + } + } + let next = self.supported_modes.basic_modes[idx]; + + self.config.read(); + // if self.config.builtins.contains_key(&next) { + self.config.current_mode = next; + self.write_current_config_mode()?; + // } + self.config.write(); + } + + Ok(()) + } + + fn write_mode(&mut self, mode: &AuraEffect) -> Result<(), RogError> { + if let LEDNode::KbdLed(platform) = &self.led_node { + let buf = [ + 1, + mode.mode as u8, + mode.colour1.0, + mode.colour1.1, + mode.colour1.2, + mode.speed as u8, + ]; + platform.set_kbd_rgb_mode(&buf)?; + } else if let LEDNode::Rog(hid_raw) = &self.led_node { + let bytes: [u8; LED_MSG_LEN] = mode.into(); + hid_raw.write_bytes(&bytes)?; + hid_raw.write_bytes(&LED_SET)?; + // Changes won't persist unless apply is set + hid_raw.write_bytes(&LED_APPLY)?; + } else { + return Err(RogError::NoAuraKeyboard); + } + self.per_key_mode_active = false; + Ok(()) + } + + pub(super) fn write_current_config_mode(&mut self) -> Result<(), RogError> { + if self.config.multizone_on { + let mode = self.config.current_mode; + let mut create = false; + // There is no multizone config for this mode so create one here + // using the colours of rainbow if it exists, or first available + // mode, or random + if self.config.multizone.is_none() { + create = true; + } else if let Some(multizones) = self.config.multizone.as_ref() { + if !multizones.contains_key(&mode) { + create = true; + } + } + if create { + info!("No user-set config for zone founding, attempting a default"); + self.create_multizone_default()?; + } + + if let Some(multizones) = self.config.multizone.as_mut() { + if let Some(set) = multizones.get(&mode) { + for mode in set.clone() { + self.write_mode(&mode)?; + } + } + } + } else { + let mode = self.config.current_mode; + if let Some(effect) = self.config.builtins.get(&mode).cloned() { + self.write_mode(&effect)?; + } + } + + Ok(()) + } + + /// Create a default for the `current_mode` if multizone and no config + /// exists. + fn create_multizone_default(&mut self) -> Result<(), RogError> { + let mut default = vec![]; + for (i, tmp) in self.supported_modes.basic_zones.iter().enumerate() { + default.push(AuraEffect { + mode: self.config.current_mode, + zone: *tmp, + colour1: *GRADIENT.get(i).unwrap_or(&GRADIENT[0]), + colour2: *GRADIENT.get(GRADIENT.len() - i).unwrap_or(&GRADIENT[6]), + speed: Speed::Med, + direction: Direction::Left, + }); + } + if default.is_empty() { + return Err(RogError::AuraEffectNotSupported); + } + + if let Some(multizones) = self.config.multizone.as_mut() { + multizones.insert(self.config.current_mode, default); + } else { + let mut tmp = BTreeMap::new(); + tmp.insert(self.config.current_mode, default); + self.config.multizone = Some(tmp); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rog_aura::aura_detection::LaptopLedData; + use rog_aura::usb::AuraDevice; + use rog_aura::{AuraEffect, AuraModeNum, AuraZone, Colour}; + use rog_platform::keyboard_led::KeyboardLed; + + use super::CtrlKbdLed; + use crate::ctrl_aura::config::AuraConfig; + use crate::ctrl_aura::controller::LEDNode; + + #[test] + // #[ignore = "Must be manually run due to detection stage"] + fn check_set_mode_errors() { + // Checking to ensure set_mode errors when unsupported modes are tried + let config = AuraConfig::create_default(AuraDevice::X19b6, &LaptopLedData::default()); + let supported_modes = LaptopLedData { + board_name: String::new(), + layout_name: "ga401".to_owned(), + basic_modes: vec![AuraModeNum::Static], + basic_zones: vec![], + advanced_type: rog_aura::AdvancedAuraType::None, + }; + let mut controller = CtrlKbdLed { + led_prod: AuraDevice::X19b6, + led_node: LEDNode::None, + kd_brightness: KeyboardLed::default(), + supported_modes, + flip_effect_write: false, + per_key_mode_active: false, + config, + }; + + let mut effect = AuraEffect { + colour1: Colour(0xff, 0x00, 0xff), + zone: AuraZone::None, + ..Default::default() + }; + + // This error comes from write_bytes because we don't have a keyboard node + // stored + assert_eq!( + controller + .set_effect(effect.clone()) + .unwrap_err() + .to_string(), + "No supported Aura keyboard" + ); + + effect.mode = AuraModeNum::Laser; + assert_eq!( + controller + .set_effect(effect.clone()) + .unwrap_err() + .to_string(), + "Aura effect not supported" + ); + + effect.mode = AuraModeNum::Static; + effect.zone = AuraZone::Key2; + assert_eq!( + controller + .set_effect(effect.clone()) + .unwrap_err() + .to_string(), + "Aura effect not supported" + ); + + controller.supported_modes.basic_zones.push(AuraZone::Key2); + assert_eq!( + controller.set_effect(effect).unwrap_err().to_string(), + "No supported Aura keyboard" + ); + } + + #[test] + fn create_multizone_if_no_config() { + // Checking to ensure set_mode errors when unsupported modes are tried + let config = AuraConfig::create_default(AuraDevice::X19b6, &LaptopLedData::default()); + let supported_modes = LaptopLedData { + board_name: String::new(), + layout_name: "ga401".to_owned(), + basic_modes: vec![AuraModeNum::Static], + basic_zones: vec![], + advanced_type: rog_aura::AdvancedAuraType::None, + }; + let mut controller = CtrlKbdLed { + led_prod: AuraDevice::X19b6, + led_node: LEDNode::None, + kd_brightness: KeyboardLed::default(), + supported_modes, + flip_effect_write: false, + per_key_mode_active: false, + config, + }; + + assert!(controller.config.multizone.is_none()); + assert!(controller.create_multizone_default().is_err()); + assert!(controller.config.multizone.is_none()); + + controller.supported_modes.basic_zones.push(AuraZone::Key1); + controller.supported_modes.basic_zones.push(AuraZone::Key2); + assert!(controller.create_multizone_default().is_ok()); + assert!(controller.config.multizone.is_some()); + + let m = controller.config.multizone.unwrap(); + assert!(m.contains_key(&AuraModeNum::Static)); + let e = m.get(&AuraModeNum::Static).unwrap(); + assert_eq!(e.len(), 2); + assert_eq!(e[0].zone, AuraZone::Key1); + assert_eq!(e[1].zone, AuraZone::Key2); + } + + #[test] + fn next_mode_create_multizone_if_no_config() { + // Checking to ensure set_mode errors when unsupported modes are tried + let config = AuraConfig::create_default(AuraDevice::X19b6, &LaptopLedData::default()); + let supported_modes = LaptopLedData { + board_name: String::new(), + layout_name: "ga401".to_owned(), + basic_modes: vec![AuraModeNum::Static], + basic_zones: vec![AuraZone::Key1, AuraZone::Key2], + advanced_type: rog_aura::AdvancedAuraType::None, + }; + let mut controller = CtrlKbdLed { + led_prod: AuraDevice::X19b6, + led_node: LEDNode::None, + kd_brightness: KeyboardLed::default(), + supported_modes, + flip_effect_write: false, + per_key_mode_active: false, + config, + }; + + assert!(controller.config.multizone.is_none()); + controller.config.multizone_on = true; + // This is called in toggle_mode. It will error here because we have no + // keyboard node in tests. + assert_eq!( + controller + .write_current_config_mode() + .unwrap_err() + .to_string(), + "No supported Aura keyboard" + ); + assert!(controller.config.multizone.is_some()); + + let m = controller.config.multizone.unwrap(); + assert!(m.contains_key(&AuraModeNum::Static)); + let e = m.get(&AuraModeNum::Static).unwrap(); + assert_eq!(e.len(), 2); + assert_eq!(e[0].zone, AuraZone::Key1); + assert_eq!(e[1].zone, AuraZone::Key2); + } +} diff --git a/asusd/src/ctrl_aura/mod.rs b/asusd/src/ctrl_aura/mod.rs new file mode 100644 index 00000000..2da39c6e --- /dev/null +++ b/asusd/src/ctrl_aura/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod controller; +/// Implements `CtrlTask`, `Reloadable`, `ZbusRun` +pub mod trait_impls; diff --git a/asusd/src/ctrl_aura/trait_impls.rs b/asusd/src/ctrl_aura/trait_impls.rs new file mode 100644 index 00000000..3411aed6 --- /dev/null +++ b/asusd/src/ctrl_aura/trait_impls.rs @@ -0,0 +1,331 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use async_trait::async_trait; +use config_traits::StdConfig; +use log::{error, info, warn}; +use rog_aura::advanced::UsbPackets; +use rog_aura::usb::AuraPowerDev; +use rog_aura::{AuraEffect, AuraModeNum, LedBrightness}; +use zbus::export::futures_util::lock::{Mutex, MutexGuard}; +use zbus::export::futures_util::StreamExt; +use zbus::{dbus_interface, Connection, SignalContext}; + +use super::controller::CtrlKbdLed; +use crate::error::RogError; +use crate::CtrlTask; + +pub(super) const ZBUS_PATH: &str = "/org/asuslinux/Aura"; + +#[derive(Clone)] +pub struct CtrlKbdLedZbus(pub Arc>); + +impl CtrlKbdLedZbus { + fn update_config(lock: &mut CtrlKbdLed) -> Result<(), RogError> { + let bright = lock.kd_brightness.get_brightness()?; + lock.config.read(); + lock.config.brightness = (bright as u32).into(); + lock.config.write(); + Ok(()) + } +} + +#[async_trait] +impl crate::ZbusRun for CtrlKbdLedZbus { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, ZBUS_PATH, server).await; + } +} + +/// 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) + async fn set_brightness(&mut self, brightness: LedBrightness) { + let ctrl = self.0.lock().await; + ctrl.set_brightness(brightness) + .map_err(|err| warn!("{}", err)) + .ok(); + } + + /// Set a variety of states, input is array of enum. + /// `enabled` sets if the sent array should be disabled or enabled + /// + /// ```text + /// pub struct AuraPowerDev { + /// pub x1866: Vec, + /// pub x19b6: Vec, + /// } + /// pub enum AuraDev1866 { + /// Awake, + /// Keyboard, + /// Lightbar, + /// Boot, + /// Sleep, + /// } + /// enum AuraDev19b6 { + /// BootLogo, + /// BootKeyb, + /// AwakeLogo, + /// AwakeKeyb, + /// SleepLogo, + /// SleepKeyb, + /// ShutdownLogo, + /// ShutdownKeyb, + /// AwakeBar, + /// BootBar, + /// SleepBar, + /// ShutdownBar, + /// BootRearBar, + /// AwakeRearBar, + /// SleepRearBar, + /// ShutdownRearBar, + /// } + /// ``` + async fn set_leds_power( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + options: AuraPowerDev, + enabled: bool, + ) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + for p in options.tuf { + ctrl.config.enabled.set_tuf(p, enabled); + } + for p in options.x1866 { + ctrl.config.enabled.set_0x1866(p, enabled); + } + for p in options.x19b6 { + ctrl.config.enabled.set_0x19b6(p, enabled); + } + + ctrl.config.write(); + + ctrl.set_power_states().map_err(|e| { + warn!("{}", e); + e + })?; + + Self::notify_power_states(&ctxt, &AuraPowerDev::from(&ctrl.config.enabled)) + .await + .unwrap_or_else(|err| warn!("{}", err)); + Ok(()) + } + + async fn set_led_mode( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + effect: AuraEffect, + ) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + + ctrl.set_effect(effect).map_err(|e| { + warn!("{}", e); + e + })?; + + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + Self::notify_led(&ctxt, mode.clone()) + .await + .unwrap_or_else(|err| warn!("{}", err)); + } + Ok(()) + } + + async fn next_led_mode( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + ) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + + ctrl.toggle_mode(false).map_err(|e| { + warn!("{}", e); + e + })?; + + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + Self::notify_led(&ctxt, mode.clone()) + .await + .unwrap_or_else(|err| warn!("{}", err)); + } + + Ok(()) + } + + async fn prev_led_mode( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + ) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + + ctrl.toggle_mode(true).map_err(|e| { + warn!("{}", e); + e + })?; + + if let Some(mode) = ctrl.config.builtins.get(&ctrl.config.current_mode) { + Self::notify_led(&ctxt, mode.clone()) + .await + .unwrap_or_else(|err| warn!("{}", err)); + } + + Ok(()) + } + + async fn next_led_brightness(&self) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.next_brightness().map_err(|e| { + warn!("{}", e); + e + })?; + Ok(()) + } + + async fn prev_led_brightness(&self) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.prev_brightness().map_err(|e| { + warn!("{}", e); + e + })?; + Ok(()) + } + + // As property doesn't work for AuraPowerDev (complexity of serialization?) + // #[dbus_interface(property)] + async fn leds_enabled(&self) -> AuraPowerDev { + let ctrl = self.0.lock().await; + AuraPowerDev::from(&ctrl.config.enabled) + } + + /// Return the current mode data + async fn led_mode(&self) -> AuraModeNum { + let ctrl = self.0.lock().await; + ctrl.config.current_mode + } + + /// Return a list of available modes + async fn led_modes(&self) -> BTreeMap { + let ctrl = self.0.lock().await; + ctrl.config.builtins.clone() + } + + /// On machine that have some form of either per-key keyboard or per-zone + /// this can be used to write custom effects over dbus. The input is a + /// nested `Vec>` where `Vec` is a raw USB packet + async fn direct_addressing_raw(&self, data: UsbPackets) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.write_effect_block(&data)?; + Ok(()) + } + + /// Return the current LED brightness + #[dbus_interface(property)] + async fn led_brightness(&self) -> i8 { + let ctrl = self.0.lock().await; + ctrl.get_brightness().map(|n| n as i8).unwrap_or(-1) + } + + #[dbus_interface(signal)] + async fn notify_led(signal_ctxt: &SignalContext<'_>, data: AuraEffect) -> zbus::Result<()>; + + #[dbus_interface(signal)] + async fn notify_power_states( + signal_ctxt: &SignalContext<'_>, + data: &AuraPowerDev, + ) -> zbus::Result<()>; +} + +#[async_trait] +impl CtrlTask for CtrlKbdLedZbus { + fn zbus_path() -> &'static str { + ZBUS_PATH + } + + async fn create_tasks(&self, _: SignalContext<'static>) -> Result<(), RogError> { + let load_save = |start: bool, mut lock: MutexGuard<'_, CtrlKbdLed>| { + // If waking up + if !start { + info!("CtrlKbdLedTask reloading brightness and modes"); + lock.set_brightness(lock.config.brightness) + .map_err(|e| error!("CtrlKbdLedTask: {e}")) + .ok(); + lock.write_current_config_mode() + .map_err(|e| error!("CtrlKbdLedTask: {e}")) + .ok(); + } else if start { + info!("CtrlKbdLedTask saving last brightness"); + Self::update_config(&mut lock) + .map_err(|e| error!("CtrlKbdLedTask: {e}")) + .ok(); + } + }; + + let inner1 = self.0.clone(); + let inner2 = self.0.clone(); + let inner3 = self.0.clone(); + let inner4 = self.0.clone(); + self.create_sys_event_tasks( + // Loop so that we do aquire the lock but also don't block other + // threads (prevents potential deadlocks) + move || { + let inner1 = inner1.clone(); + async move { + let lock = inner1.lock().await; + load_save(true, lock); + } + }, + move || { + let inner2 = inner2.clone(); + async move { + let lock = inner2.lock().await; + load_save(false, lock); + } + }, + move || { + let inner3 = inner3.clone(); + async move { + let lock = inner3.lock().await; + load_save(false, lock); + } + }, + move || { + let inner4 = inner4.clone(); + async move { + let lock = inner4.lock().await; + load_save(false, lock); + } + }, + ) + .await; + + let ctrl2 = self.0.clone(); + let ctrl = self.0.lock().await; + let watch = ctrl.kd_brightness.monitor_brightness()?; + tokio::spawn(async move { + let mut buffer = [0; 32]; + watch + .into_event_stream(&mut buffer) + .unwrap() + .for_each(|_| async { + if let Some(lock) = ctrl2.try_lock() { + load_save(true, lock); + } + }) + .await; + }); + + Ok(()) + } +} + +#[async_trait] +impl crate::Reloadable for CtrlKbdLedZbus { + async fn reload(&mut self) -> Result<(), RogError> { + let mut ctrl = self.0.lock().await; + ctrl.write_current_config_mode()?; + ctrl.set_power_states().map_err(|err| warn!("{err}")).ok(); + Ok(()) + } +} diff --git a/asusd/src/ctrl_platform.rs b/asusd/src/ctrl_platform.rs new file mode 100644 index 00000000..29b342cc --- /dev/null +++ b/asusd/src/ctrl_platform.rs @@ -0,0 +1,382 @@ +use std::fs::OpenOptions; +use std::io::{Read, Write}; +use std::path::Path; +use std::process::Command; +use std::sync::Arc; + +use async_trait::async_trait; +use config_traits::StdConfig; +use log::{info, warn}; +use rog_platform::platform::{AsusPlatform, GpuMode}; +use rog_platform::supported::RogBiosSupportedFunctions; +use zbus::export::futures_util::lock::Mutex; +use zbus::{dbus_interface, Connection, SignalContext}; + +use crate::config::Config; +use crate::error::RogError; +use crate::{task_watch_item, CtrlTask, GetSupported}; + +const ZBUS_PATH: &str = "/org/asuslinux/Platform"; +const ASUS_POST_LOGO_SOUND: &str = + "/sys/firmware/efi/efivars/AsusPostLogoSound-607005d5-3f75-4b2e-98f0-85ba66797a3e"; + +#[derive(Clone)] +pub struct CtrlPlatform { + platform: AsusPlatform, + config: Arc>, +} + +impl GetSupported for CtrlPlatform { + type A = RogBiosSupportedFunctions; + + fn get_supported() -> Self::A { + let mut panel_overdrive = false; + let mut dgpu_disable = false; + let mut egpu_enable = false; + let mut gpu_mux = false; + + if let Ok(platform) = AsusPlatform::new() { + panel_overdrive = platform.has_panel_od(); + dgpu_disable = platform.has_dgpu_disable(); + egpu_enable = platform.has_egpu_enable(); + gpu_mux = platform.has_gpu_mux_mode(); + } + + RogBiosSupportedFunctions { + post_sound: Path::new(ASUS_POST_LOGO_SOUND).exists(), + gpu_mux, + panel_overdrive, + dgpu_disable, + egpu_enable, + } + } +} + +impl CtrlPlatform { + pub fn new(config: Arc>) -> Result { + let platform = AsusPlatform::new()?; + + if !platform.has_gpu_mux_mode() { + info!("G-Sync Switchable Graphics or GPU MUX not detected"); + info!("Standard graphics switching will still work."); + } + + if Path::new(ASUS_POST_LOGO_SOUND).exists() { + CtrlPlatform::set_path_mutable(ASUS_POST_LOGO_SOUND)?; + } else { + info!("Switch for POST boot sound not detected"); + } + + Ok(CtrlPlatform { platform, config }) + } + + fn set_path_mutable(path: &str) -> Result<(), RogError> { + let output = Command::new("/usr/bin/chattr") + .arg("-i") + .arg(path) + .output() + .map_err(|err| RogError::Path(path.into(), err))?; + info!("Set {} writeable: status: {}", path, output.status); + Ok(()) + } + + fn set_gfx_mode(&self, mode: GpuMode) -> Result<(), RogError> { + self.platform.set_gpu_mux_mode(mode.to_mux_attr())?; + // self.update_initramfs(enable)?; + if mode == GpuMode::Discrete { + info!("Set system-level graphics mode: Dedicated Nvidia"); + } else { + info!("Set system-level graphics mode: Optimus"); + } + Ok(()) + } + + pub fn get_boot_sound() -> Result { + let data = std::fs::read(ASUS_POST_LOGO_SOUND) + .map_err(|err| RogError::Read(ASUS_POST_LOGO_SOUND.into(), err))?; + + let idx = data.len() - 1; + Ok(data[idx] as i8) + } + + pub(super) fn set_boot_sound(on: bool) -> Result<(), RogError> { + let path = ASUS_POST_LOGO_SOUND; + let mut file = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .map_err(|err| RogError::Path(path.into(), err))?; + + let mut data = Vec::new(); + #[allow(clippy::verbose_file_reads)] + file.read_to_end(&mut data) + .map_err(|err| RogError::Read(path.into(), err))?; + + let idx = data.len() - 1; + if on { + data[idx] = 1; + info!("Set boot POST sound on"); + } else { + data[idx] = 0; + info!("Set boot POST sound off"); + } + file.write_all(&data) + .map_err(|err| RogError::Path(path.into(), err))?; + + Ok(()) + } + + fn set_panel_overdrive(&self, enable: bool) -> Result<(), RogError> { + self.platform.set_panel_od(enable).map_err(|err| { + warn!("CtrlRogBios: set_panel_overdrive {}", err); + err + })?; + Ok(()) + } +} + +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlPlatform { + async fn set_gpu_mux_mode( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + mode: GpuMode, + ) { + self.set_gfx_mode(mode) + .map_err(|err| { + warn!("CtrlRogBios: set_gpu_mux_mode {}", err); + err + }) + .ok(); + Self::notify_gpu_mux_mode(&ctxt, mode).await.ok(); + } + + fn gpu_mux_mode(&self) -> GpuMode { + match self.platform.get_gpu_mux_mode() { + Ok(m) => GpuMode::from_mux(m as u8), + Err(e) => { + warn!("CtrlRogBios: get_gfx_mode {}", e); + GpuMode::Error + } + } + } + + #[dbus_interface(signal)] + async fn notify_gpu_mux_mode( + signal_ctxt: &SignalContext<'_>, + mode: GpuMode, + ) -> zbus::Result<()> { + } + + async fn set_post_boot_sound( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + on: bool, + ) { + Self::set_boot_sound(on) + .map_err(|err| { + warn!("CtrlRogBios: set_post_boot_sound {}", err); + err + }) + .ok(); + Self::notify_post_boot_sound(&ctxt, on).await.ok(); + } + + fn post_boot_sound(&self) -> i8 { + Self::get_boot_sound() + .map_err(|err| { + warn!("CtrlRogBios: get_boot_sound {}", err); + err + }) + .unwrap_or(-1) + } + + #[dbus_interface(signal)] + async fn notify_post_boot_sound(ctxt: &SignalContext<'_>, on: bool) -> zbus::Result<()> {} + + async fn set_panel_od( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + overdrive: bool, + ) { + match self.platform.set_panel_od(overdrive) { + Ok(_) => { + if let Some(mut lock) = self.config.try_lock() { + lock.panel_od = overdrive; + lock.write(); + } + Self::notify_panel_od(&ctxt, overdrive).await.ok(); + } + Err(err) => warn!("CtrlRogBios: set_panel_overdrive {}", err), + }; + } + + /// Get the `panel_od` value from platform. Updates the stored value in + /// internal config also. + fn panel_od(&self) -> bool { + let od = self + .platform + .get_panel_od() + .map_err(|err| { + warn!("CtrlRogBios: get_panel_od {}", err); + err + }) + .unwrap_or(false); + if let Some(mut lock) = self.config.try_lock() { + lock.panel_od = od; + lock.write(); + } + od + } + + #[dbus_interface(signal)] + async fn notify_panel_od(signal_ctxt: &SignalContext<'_>, overdrive: bool) -> zbus::Result<()> { + } + + async fn set_dgpu_disable( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + disable: bool, + ) { + match self.platform.set_dgpu_disable(disable) { + Ok(_) => { + Self::notify_dgpu_disable(&ctxt, disable).await.ok(); + } + Err(err) => warn!("CtrlRogBios: set_dgpu_disable {}", err), + }; + } + + fn dgpu_disable(&self) -> bool { + self.platform + .get_dgpu_disable() + .map_err(|err| { + warn!("CtrlRogBios: get_dgpu_disable {}", err); + err + }) + .unwrap_or(false) + } + + #[dbus_interface(signal)] + async fn notify_dgpu_disable( + signal_ctxt: &SignalContext<'_>, + disable: bool, + ) -> zbus::Result<()> { + } + + async fn set_egpu_enable( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + enable: bool, + ) { + match self.platform.set_egpu_enable(enable) { + Ok(_) => { + Self::notify_egpu_enable(&ctxt, enable).await.ok(); + } + Err(err) => warn!("CtrlRogBios: set_egpu_enable {}", err), + }; + } + + fn egpu_enable(&self) -> bool { + self.platform + .get_egpu_enable() + .map_err(|err| { + warn!("CtrlRogBios: get_egpu_enable {}", err); + err + }) + .unwrap_or(false) + } + + #[dbus_interface(signal)] + async fn notify_egpu_enable(signal_ctxt: &SignalContext<'_>, enable: bool) -> zbus::Result<()> { + } +} + +#[async_trait] +impl crate::ZbusRun for CtrlPlatform { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, "/org/asuslinux/Platform", server).await; + } +} + +#[async_trait] +impl crate::Reloadable for CtrlPlatform { + async fn reload(&mut self) -> Result<(), RogError> { + if self.platform.has_panel_od() { + let p = if let Some(lock) = self.config.try_lock() { + lock.panel_od + } else { + false + }; + self.set_panel_overdrive(p)?; + } + Ok(()) + } +} + +impl CtrlPlatform { + task_watch_item!(panel_od platform); + + task_watch_item!(dgpu_disable platform); + + task_watch_item!(egpu_enable platform); + // NOTE: see note further below + // task_watch_item!(gpu_mux_mode platform); +} + +#[async_trait] +impl CtrlTask for CtrlPlatform { + fn zbus_path() -> &'static str { + ZBUS_PATH + } + + async fn create_tasks(&self, signal_ctxt: SignalContext<'static>) -> Result<(), RogError> { + let platform1 = self.clone(); + let platform2 = self.clone(); + self.create_sys_event_tasks( + move || async { {} }, + move || { + let platform1 = platform1.clone(); + async move { + info!("CtrlRogBios reloading panel_od"); + let lock = platform1.config.lock().await; + if platform1.platform.has_panel_od() { + platform1 + .set_panel_overdrive(lock.panel_od) + .map_err(|err| { + warn!("CtrlCharge: set_limit {}", err); + err + }) + .ok(); + } + } + }, + move || async { {} }, + move || { + let platform2 = platform2.clone(); + async move { + info!("CtrlRogBios reloading panel_od"); + let lock = platform2.config.lock().await; + if platform2.platform.has_panel_od() { + platform2 + .set_panel_overdrive(lock.panel_od) + .map_err(|err| { + warn!("CtrlCharge: set_limit {}", err); + err + }) + .ok(); + } + } + }, + ) + .await; + + self.watch_panel_od(signal_ctxt.clone()).await?; + self.watch_dgpu_disable(signal_ctxt.clone()).await?; + self.watch_egpu_enable(signal_ctxt.clone()).await?; + // NOTE: Can't have this as a watch because on a write to it, it reverts back to + // booted-with value as it does not actually change until reboot. + // self.watch_gpu_mux_mode(signal_ctxt.clone()).await?; + + Ok(()) + } +} diff --git a/asusd/src/ctrl_power.rs b/asusd/src/ctrl_power.rs new file mode 100644 index 00000000..df8b6814 --- /dev/null +++ b/asusd/src/ctrl_power.rs @@ -0,0 +1,287 @@ +use std::process::Command; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use config_traits::StdConfig; +use log::{error, info, warn}; +use rog_platform::power::AsusPower; +use rog_platform::supported::ChargeSupportedFunctions; +use systemd_zbus::{ManagerProxy as SystemdProxy, Mode, UnitFileState}; +use tokio::time::sleep; +use zbus::export::futures_util::lock::Mutex; +use zbus::{dbus_interface, Connection, SignalContext}; + +use crate::config::Config; +use crate::error::RogError; +use crate::{task_watch_item, CtrlTask, GetSupported}; + +const ZBUS_PATH: &str = "/org/asuslinux/Power"; +const NVIDIA_POWERD: &str = "nvidia-powerd.service"; + +impl GetSupported for CtrlPower { + type A = ChargeSupportedFunctions; + + fn get_supported() -> Self::A { + ChargeSupportedFunctions { + charge_level_set: if let Ok(power) = AsusPower::new() { + power.has_charge_control_end_threshold() + } else { + false + }, + } + } +} + +#[derive(Clone)] +pub struct CtrlPower { + power: AsusPower, + config: Arc>, +} + +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl CtrlPower { + async fn set_charge_control_end_threshold( + &mut self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + limit: u8, + ) -> zbus::fdo::Result<()> { + if !(20..=100).contains(&limit) { + return Err(RogError::ChargeLimit(limit))?; + } + self.set(limit) + .map_err(|err| { + warn!("CtrlCharge: set_limit {}", err); + err + }) + .ok(); + Self::notify_charge_control_end_threshold(&ctxt, limit) + .await + .ok(); + Ok(()) + } + + fn charge_control_end_threshold(&self) -> u8 { + loop { + if let Some(mut config) = self.config.try_lock() { + let limit = self + .power + .get_charge_control_end_threshold() + .map_err(|err| { + warn!("CtrlCharge: get_charge_control_end_threshold {}", err); + err + }) + .unwrap_or(100); + + config.read(); + config.bat_charge_limit = limit; + config.write(); + + return config.bat_charge_limit; + } + } + } + + fn mains_online(&self) -> bool { + if self.power.has_online() { + if let Ok(v) = self.power.get_online() { + return v == 1; + } + } + false + } + + #[dbus_interface(signal)] + async fn notify_charge_control_end_threshold( + ctxt: &SignalContext<'_>, + limit: u8, + ) -> zbus::Result<()>; + + #[dbus_interface(signal)] + async fn notify_mains_online(ctxt: &SignalContext<'_>, on: bool) -> zbus::Result<()>; +} + +#[async_trait] +impl crate::ZbusRun for CtrlPower { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, ZBUS_PATH, server).await; + } +} + +#[async_trait] +impl crate::Reloadable for CtrlPower { + async fn reload(&mut self) -> Result<(), RogError> { + if let Some(mut config) = self.config.try_lock() { + config.read(); + self.set(config.bat_charge_limit)?; + } + Ok(()) + } +} + +impl CtrlPower { + task_watch_item!(charge_control_end_threshold power); + + pub fn new(config: Arc>) -> Result { + Ok(CtrlPower { + power: AsusPower::new()?, + config, + }) + } + + pub(super) fn set(&self, limit: u8) -> Result<(), RogError> { + if !(20..=100).contains(&limit) { + return Err(RogError::ChargeLimit(limit)); + } + + self.power.set_charge_control_end_threshold(limit)?; + + info!("Battery charge limit: {}", limit); + + if let Some(mut config) = self.config.try_lock() { + config.read(); + config.bat_charge_limit = limit; + config.write(); + } + + Ok(()) + } +} + +#[async_trait] +impl CtrlTask for CtrlPower { + fn zbus_path() -> &'static str { + ZBUS_PATH + } + + async fn create_tasks(&self, signal_ctxt: SignalContext<'static>) -> Result<(), RogError> { + let conn = zbus::Connection::system().await?; + let sysd1 = SystemdProxy::new(&conn).await?; + let sysd2 = sysd1.clone(); + let sysd3 = sysd1.clone(); + + let power1 = self.clone(); + let power2 = self.clone(); + self.create_sys_event_tasks( + move || async {}, + move || { + let power = power1.clone(); + let sysd = sysd1.clone(); + async move { + info!("CtrlCharge reloading charge limit"); + let lock = power.config.lock().await; + power + .set(lock.bat_charge_limit) + .map_err(|err| { + warn!("CtrlCharge: set_limit {}", err); + err + }) + .ok(); + + if lock.disable_nvidia_powerd_on_battery { + if let Ok(value) = power.power.get_online() { + do_nvidia_powerd_action(&sysd, value == 1).await; + } + } + } + }, + move || async {}, + move || { + let power = power2.clone(); + let sysd = sysd2.clone(); + async move { + info!("CtrlCharge reloading charge limit"); + let lock = power.config.lock().await; + power + .set(lock.bat_charge_limit) + .map_err(|err| { + warn!("CtrlCharge: set_limit {}", err); + err + }) + .ok(); + + if lock.disable_nvidia_powerd_on_battery { + if let Ok(value) = power.power.get_online() { + do_nvidia_powerd_action(&sysd, value == 1).await; + } + } + } + }, + ) + .await; + + let config = self.config.clone(); + self.watch_charge_control_end_threshold(signal_ctxt.clone()) + .await?; + + let ctrl = self.clone(); + tokio::spawn(async move { + let mut online = 10; + loop { + if let Ok(value) = ctrl.power.get_online() { + if online != value { + online = value; + let mut config = config.lock().await; + config.read(); + + if config.disable_nvidia_powerd_on_battery { + do_nvidia_powerd_action(&sysd3, value == 1).await; + } + + Self::notify_mains_online(&signal_ctxt, value == 1) + .await + .unwrap(); + + let mut prog: Vec<&str> = Vec::new(); + if value == 1 { + // AC ONLINE + prog = config.ac_command.split_whitespace().collect(); + } else if value == 0 { + // BATTERY + prog = config.bat_command.split_whitespace().collect(); + } + + if prog.len() > 1 { + let mut cmd = Command::new(prog[0]); + for arg in prog.iter().skip(1) { + cmd.arg(*arg); + } + if let Err(e) = cmd.spawn() { + if value == 1 { + error!("AC power command error: {e}"); + } else { + error!("Battery power command error: {e}"); + } + } + } + } + } + // The inotify doesn't pick up events when the kernel changes internal value + // so we need to watch it with a thread and sleep unfortunately + sleep(Duration::from_secs(1)).await; + } + }); + + Ok(()) + } +} + +async fn do_nvidia_powerd_action(proxy: &SystemdProxy<'_>, ac_on: bool) { + if let Ok(res) = proxy.get_unit_file_state(NVIDIA_POWERD).await { + if res == UnitFileState::Enabled { + if ac_on { + proxy + .start_unit(NVIDIA_POWERD, Mode::Replace) + .await + .map_err(|e| error!("Error stopping {NVIDIA_POWERD}, {e:?}")) + .ok(); + } else { + proxy + .stop_unit(NVIDIA_POWERD, Mode::Replace) + .await + .map_err(|e| error!("Error stopping {NVIDIA_POWERD}, {e:?}")) + .ok(); + } + } + } +} diff --git a/asusd/src/ctrl_profiles/config.rs b/asusd/src/ctrl_profiles/config.rs new file mode 100644 index 00000000..a66f8a75 --- /dev/null +++ b/asusd/src/ctrl_profiles/config.rs @@ -0,0 +1,60 @@ +use std::path::PathBuf; + +use config_traits::{StdConfig, StdConfigLoad}; +use rog_profiles::fan_curve_set::FanCurveSet; +use rog_profiles::Profile; +use serde_derive::{Deserialize, Serialize}; + +use crate::CONFIG_PATH_BASE; + +const CONFIG_FILE: &str = "profile.ron"; +const CONFIG_FAN_FILE: &str = "fan_curves.ron"; + +#[derive(Deserialize, Serialize, Debug)] +pub struct ProfileConfig { + /// For restore on boot + pub active_profile: Profile, +} + +impl StdConfig for ProfileConfig { + fn new() -> Self { + Self { + active_profile: Profile::Balanced, + } + } + + fn config_dir() -> std::path::PathBuf { + PathBuf::from(CONFIG_PATH_BASE) + } + + fn file_name(&self) -> String { + CONFIG_FILE.to_owned() + } +} + +impl StdConfigLoad for ProfileConfig {} + +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct FanCurveConfig { + pub balanced: FanCurveSet, + pub performance: FanCurveSet, + pub quiet: FanCurveSet, +} + +impl StdConfig for FanCurveConfig { + /// Create a new config. The defaults are zeroed so the device must be read + /// to get the actual device defaults. + fn new() -> Self { + Self::default() + } + + fn config_dir() -> std::path::PathBuf { + PathBuf::from(CONFIG_PATH_BASE) + } + + fn file_name(&self) -> String { + CONFIG_FAN_FILE.to_owned() + } +} + +impl StdConfigLoad for FanCurveConfig {} diff --git a/asusd/src/ctrl_profiles/controller.rs b/asusd/src/ctrl_profiles/controller.rs new file mode 100644 index 00000000..b6752bc1 --- /dev/null +++ b/asusd/src/ctrl_profiles/controller.rs @@ -0,0 +1,191 @@ +use config_traits::{StdConfig, StdConfigLoad}; +use log::{info, warn}; +use rog_platform::platform::AsusPlatform; +use rog_platform::supported::PlatformProfileFunctions; +use rog_profiles::error::ProfileError; +use rog_profiles::{FanCurveProfiles, Profile}; + +use super::config::{FanCurveConfig, ProfileConfig}; +use crate::error::RogError; +use crate::GetSupported; + +// TODO: macro wrapper for warn/info/error log macros to add module name +const MOD_NAME: &str = "CtrlPlatformProfile"; + +pub struct FanCurves { + config_file: FanCurveConfig, + profiles: FanCurveProfiles, +} + +impl FanCurves { + pub fn update_profiles_from_config(&mut self) { + self.profiles.balanced = self.config_file.balanced.clone(); + self.profiles.performance = self.config_file.performance.clone(); + self.profiles.quiet = self.config_file.quiet.clone(); + } + + pub fn update_config_from_profiles(&mut self) { + self.config_file.balanced = self.profiles.balanced.clone(); + self.config_file.performance = self.profiles.performance.clone(); + self.config_file.quiet = self.profiles.quiet.clone(); + } + + pub fn profiles(&self) -> &FanCurveProfiles { + &self.profiles + } + + pub fn profiles_mut(&mut self) -> &mut FanCurveProfiles { + &mut self.profiles + } +} + +pub struct CtrlPlatformProfile { + pub profile_config: ProfileConfig, + pub fan_curves: Option, + pub platform: AsusPlatform, +} + +impl GetSupported for CtrlPlatformProfile { + type A = PlatformProfileFunctions; + + fn get_supported() -> Self::A { + if !Profile::is_platform_profile_supported() { + warn!( + "platform_profile kernel interface not found, your laptop does not support this, \ + or the interface is missing." + ); + } + + let res = FanCurveProfiles::is_supported(); + let mut fan_curve_supported = res.is_err(); + if let Ok(r) = res { + fan_curve_supported = r; + }; + + if !fan_curve_supported { + info!( + "fan curves kernel interface not found, your laptop does not support this, or the \ + interface is missing." + ); + } + + PlatformProfileFunctions { + platform_profile: Profile::is_platform_profile_supported(), + fan_curves: fan_curve_supported, + } + } +} + +impl CtrlPlatformProfile { + pub fn new(config: ProfileConfig) -> Result { + let platform = AsusPlatform::new()?; + if platform.has_platform_profile() || platform.has_throttle_thermal_policy() { + info!("{MOD_NAME}: Device has profile control available"); + + let mut controller = CtrlPlatformProfile { + profile_config: config, + fan_curves: None, + platform, + }; + if FanCurveProfiles::get_device().is_ok() { + info!("{MOD_NAME}: Device has fan curves available"); + let fan_config = FanCurveConfig::new(); + // Only do defaults if the config doesn't already exist + if !fan_config.file_path().exists() { + info!("{MOD_NAME}: Fetching default fan curves"); + controller.fan_curves = Some(FanCurves { + config_file: fan_config, + profiles: FanCurveProfiles::default(), + }); + for _ in [Profile::Balanced, Profile::Performance, Profile::Quiet] { + // For each profile we need to switch to it before we + // can read the existing values from hardware. The ACPI method used + // for this is what limits us. + controller.set_next_profile()?; + // Make sure to set the baseline to default + controller.set_active_curve_to_defaults()?; + let active = Profile::get_active_profile().unwrap_or(Profile::Balanced); + + if let Some(curves) = controller.fan_curves.as_ref() { + info!( + "{MOD_NAME}: {active:?}: {}", + String::from(curves.profiles().get_fan_curves_for(active)) + ); + } + } + if let Some(curves) = controller.fan_curves.as_ref() { + curves.config_file.write(); + } + } else { + info!("{MOD_NAME}: Fan curves previously stored, loading..."); + let mut fan_curves = FanCurves { + config_file: fan_config.load(), + profiles: FanCurveProfiles::default(), + }; + fan_curves.update_profiles_from_config(); + controller.fan_curves = Some(fan_curves); + } + } + + return Ok(controller); + } + + Err(ProfileError::NotSupported.into()) + } + + pub fn save_config(&mut self) { + self.profile_config.write(); + if let Some(fans) = self.fan_curves.as_mut() { + fans.update_config_from_profiles(); + fans.config_file.write(); // config write + } + } + + /// Toggle to next profile in list. This will first read the config, switch, + /// then write out + pub(super) fn set_next_profile(&mut self) -> Result<(), RogError> { + // Read first just incase the user has modified the config before calling this + match self.profile_config.active_profile { + Profile::Balanced => { + Profile::set_profile(Profile::Performance)?; + self.profile_config.active_profile = Profile::Performance; + } + Profile::Performance => { + Profile::set_profile(Profile::Quiet)?; + self.profile_config.active_profile = Profile::Quiet; + } + Profile::Quiet => { + Profile::set_profile(Profile::Balanced)?; + self.profile_config.active_profile = Profile::Balanced; + } + } + self.write_profile_curve_to_platform()?; + Ok(()) + } + + /// Set the curve for the active profile active + pub(super) fn write_profile_curve_to_platform(&mut self) -> Result<(), RogError> { + if let Some(curves) = &mut self.fan_curves { + if let Ok(mut device) = FanCurveProfiles::get_device() { + curves.profiles_mut().write_profile_curve_to_platform( + self.profile_config.active_profile, + &mut device, + )?; + } + } + Ok(()) + } + + pub(super) fn set_active_curve_to_defaults(&mut self) -> Result<(), RogError> { + if let Some(curves) = self.fan_curves.as_mut() { + if let Ok(mut device) = FanCurveProfiles::get_device() { + curves.profiles_mut().set_active_curve_to_defaults( + self.profile_config.active_profile, + &mut device, + )?; + curves.update_config_from_profiles(); + } + } + Ok(()) + } +} diff --git a/asusd/src/ctrl_profiles/mod.rs b/asusd/src/ctrl_profiles/mod.rs new file mode 100644 index 00000000..d4212ff9 --- /dev/null +++ b/asusd/src/ctrl_profiles/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod controller; +/// Implements `CtrlTask`, Reloadable, `ZbusRun` +pub mod trait_impls; diff --git a/asusd/src/ctrl_profiles/trait_impls.rs b/asusd/src/ctrl_profiles/trait_impls.rs new file mode 100644 index 00000000..76b5c41c --- /dev/null +++ b/asusd/src/ctrl_profiles/trait_impls.rs @@ -0,0 +1,311 @@ +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use config_traits::StdConfig; +use log::{error, info, warn}; +use rog_profiles::fan_curve_set::{CurveData, FanCurveSet}; +use rog_profiles::{FanCurveProfiles, Profile}; +use zbus::export::futures_util::lock::Mutex; +use zbus::export::futures_util::StreamExt; +use zbus::fdo::Error; +use zbus::{dbus_interface, Connection, SignalContext}; + +use super::controller::CtrlPlatformProfile; +use crate::error::RogError; +use crate::CtrlTask; + +const MOD_NAME: &str = "ProfileZbus"; + +const ZBUS_PATH: &str = "/org/asuslinux/Profile"; +const UNSUPPORTED_MSG: &str = + "Fan curves are not supported on this laptop or you require a patched kernel"; + +#[derive(Clone)] +pub struct ProfileZbus(pub Arc>); + +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl ProfileZbus { + /// Fetch profile names + fn profiles(&mut self) -> zbus::fdo::Result> { + if let Ok(profiles) = Profile::get_profile_names() { + return Ok(profiles); + } + Err(Error::Failed( + "Failed to get all profile details".to_owned(), + )) + } + + /// Toggle to next platform_profile. Names provided by `Profiles`. + /// If fan-curves are supported will also activate a fan curve for profile. + async fn next_profile(&mut self, #[zbus(signal_context)] ctxt: SignalContext<'_>) { + let mut ctrl = self.0.lock().await; + ctrl.set_next_profile() + .unwrap_or_else(|err| warn!("{MOD_NAME}: {}", err)); + ctrl.save_config(); + + Self::notify_profile(&ctxt, ctrl.profile_config.active_profile) + .await + .ok(); + } + + /// Fetch the active profile name + async fn active_profile(&mut self) -> zbus::fdo::Result { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + Ok(ctrl.profile_config.active_profile) + } + + /// Set this platform_profile name as active + async fn set_active_profile( + &self, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + profile: Profile, + ) { + let mut ctrl = self.0.lock().await; + // Read first just incase the user has modified the config before calling this + ctrl.profile_config.read(); + Profile::set_profile(profile) + .map_err(|e| warn!("{MOD_NAME}: set_profile, {}", e)) + .ok(); + ctrl.profile_config.active_profile = profile; + ctrl.write_profile_curve_to_platform() + .map_err(|e| warn!("{MOD_NAME}: write_profile_curve_to_platform, {}", e)) + .ok(); + + ctrl.save_config(); + + Self::notify_profile(&ctxt, ctrl.profile_config.active_profile) + .await + .ok(); + } + + /// Get a list of profiles that have fan-curves enabled. + async fn enabled_fan_profiles(&mut self) -> zbus::fdo::Result> { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + if let Some(curves) = &mut ctrl.fan_curves { + return Ok(curves.profiles().get_enabled_curve_profiles()); + } + Err(Error::Failed(UNSUPPORTED_MSG.to_owned())) + } + + /// Set a profile fan curve enabled status. Will also activate a fan curve + /// if in the same profile mode + async fn set_fan_curve_enabled( + &mut self, + profile: Profile, + enabled: bool, + ) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + if let Some(curves) = &mut ctrl.fan_curves { + curves + .profiles_mut() + .set_profile_curve_enabled(profile, enabled); + + ctrl.write_profile_curve_to_platform() + .map_err(|e| warn!("{MOD_NAME}: write_profile_curve_to_platform, {}", e)) + .ok(); + + ctrl.save_config(); + Ok(()) + } else { + Err(Error::Failed(UNSUPPORTED_MSG.to_owned())) + } + } + + /// Get the fan-curve data for the currently active Profile + async fn fan_curve_data(&mut self, profile: Profile) -> zbus::fdo::Result { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + if let Some(curves) = &mut ctrl.fan_curves { + let curve = curves.profiles().get_fan_curves_for(profile); + return Ok(curve.clone()); + } + Err(Error::Failed(UNSUPPORTED_MSG.to_owned())) + } + + /// Set the fan curve for the specified profile. + /// Will also activate the fan curve if the user is in the same mode. + async fn set_fan_curve(&self, profile: Profile, curve: CurveData) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + if let Some(curves) = &mut ctrl.fan_curves { + curves + .profiles_mut() + .save_fan_curve(curve, profile) + .map_err(|err| zbus::fdo::Error::Failed(err.to_string()))?; + } else { + return Err(Error::Failed(UNSUPPORTED_MSG.to_owned())); + } + ctrl.write_profile_curve_to_platform() + .map_err(|e| warn!("{MOD_NAME}: Profile::set_profile, {}", e)) + .ok(); + ctrl.save_config(); + + Ok(()) + } + + /// Reset the stored (self) and device curve to the defaults of the + /// platform. + /// + /// Each platform_profile has a different default and the defualt can be + /// read only for the currently active profile. + async fn set_active_curve_to_defaults(&self) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + ctrl.set_active_curve_to_defaults() + .map_err(|e| warn!("{MOD_NAME}: Profile::set_active_curve_to_defaults, {}", e)) + .ok(); + ctrl.save_config(); + Ok(()) + } + + /// Reset the stored (self) and device curve to the defaults of the + /// platform. + /// + /// Each platform_profile has a different default and the defualt can be + /// read only for the currently active profile. + async fn reset_profile_curves(&self, profile: Profile) -> zbus::fdo::Result<()> { + let mut ctrl = self.0.lock().await; + ctrl.profile_config.read(); + let active = Profile::get_active_profile().unwrap_or(Profile::Balanced); + + Profile::set_profile(profile) + .map_err(|e| warn!("{MOD_NAME}: set_profile, {}", e)) + .ok(); + ctrl.set_active_curve_to_defaults() + .map_err(|e| warn!("{MOD_NAME}: Profile::set_active_curve_to_defaults, {}", e)) + .ok(); + + Profile::set_profile(active) + .map_err(|e| warn!("{MOD_NAME}: set_profile, {}", e)) + .ok(); + ctrl.save_config(); + Ok(()) + } + + #[dbus_interface(signal)] + async fn notify_profile(signal_ctxt: &SignalContext<'_>, profile: Profile) -> zbus::Result<()> { + } +} + +#[async_trait] +impl crate::ZbusRun for ProfileZbus { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, ZBUS_PATH, server).await; + } +} + +#[async_trait] +impl CtrlTask for ProfileZbus { + fn zbus_path() -> &'static str { + ZBUS_PATH + } + + async fn create_tasks(&self, signal_ctxt: SignalContext<'static>) -> Result<(), RogError> { + let ctrl = self.0.clone(); + let sig_ctx = signal_ctxt.clone(); + let watch = self + .0 + .lock() + .await + .platform + .monitor_throttle_thermal_policy()?; + + tokio::spawn(async move { + let mut buffer = [0; 32]; + if let Ok(stream) = watch.into_event_stream(&mut buffer) { + stream + .for_each(|_| async { + let mut lock = ctrl.lock().await; + if let Ok(profile) = + lock.platform.get_throttle_thermal_policy().map_err(|e| { + error!("{MOD_NAME}: get_throttle_thermal_policy error: {e}"); + }) + { + let new_profile = Profile::from_throttle_thermal_policy(profile); + if new_profile != lock.profile_config.active_profile { + info!("{MOD_NAME}: platform_profile changed to {new_profile}"); + lock.profile_config.active_profile = new_profile; + lock.write_profile_curve_to_platform().unwrap(); + lock.save_config(); + Profile::set_profile(lock.profile_config.active_profile) + .map_err(|e| { + error!("Profile::set_profile() error: {e}"); + }) + .ok(); + } + Self::notify_profile(&sig_ctx, lock.profile_config.active_profile) + .await + .ok(); + } + }) + .await; + } + }); + + let ctrl = self.0.clone(); + let watch = self.0.lock().await.platform.monitor_platform_profile()?; + + tokio::spawn(async move { + let mut buffer = [0; 32]; + if let Ok(stream) = watch.into_event_stream(&mut buffer) { + stream + .for_each(|_| async { + let mut lock = ctrl.lock().await; + if let Ok(profile) = lock.platform.get_platform_profile().map_err(|e| { + error!("get_platform_profile error: {e}"); + }) { + if let Ok(new_profile) = Profile::from_str(&profile).map_err(|e| { + error!("Profile::from_str(&profile) error: {e}"); + }) { + if new_profile != lock.profile_config.active_profile { + info!("{MOD_NAME}: platform_profile changed to {new_profile}"); + lock.profile_config.active_profile = new_profile; + lock.write_profile_curve_to_platform().unwrap(); + lock.save_config(); + Profile::set_profile(lock.profile_config.active_profile) + .map_err(|e| { + error!("Profile::set_profile() error: {e}"); + }) + .ok(); + } + Self::notify_profile( + &signal_ctxt, + lock.profile_config.active_profile, + ) + .await + .ok(); + } + } + }) + .await; + } + }); + + Ok(()) + } +} + +#[async_trait] +impl crate::Reloadable for ProfileZbus { + /// Fetch the active profile and use that to set all related components up + async fn reload(&mut self) -> Result<(), RogError> { + let mut ctrl = self.0.lock().await; + let active = ctrl.profile_config.active_profile; + if let Some(curves) = &mut ctrl.fan_curves { + if let Ok(mut device) = FanCurveProfiles::get_device() { + // There is a possibility that the curve was default zeroed, so this call + // initialises the data from system read and we need to save it + // after + curves + .profiles_mut() + .write_profile_curve_to_platform(active, &mut device)?; + ctrl.profile_config.write(); + } + } + Ok(()) + } +} diff --git a/asusd/src/ctrl_supported.rs b/asusd/src/ctrl_supported.rs new file mode 100644 index 00000000..3a94db75 --- /dev/null +++ b/asusd/src/ctrl_supported.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; +use serde_derive::{Deserialize, Serialize}; +use zbus::zvariant::Type; +use zbus::{dbus_interface, Connection}; + +use crate::ctrl_anime::CtrlAnime; +use crate::ctrl_aura::controller::CtrlKbdLed; +use crate::ctrl_platform::CtrlPlatform; +use crate::ctrl_power::CtrlPower; +use crate::ctrl_profiles::controller::CtrlPlatformProfile; +use crate::GetSupported; + +#[derive(Serialize, Deserialize, Debug, Type)] +pub struct SupportedFunctions(rog_platform::supported::SupportedFunctions); + +#[dbus_interface(name = "org.asuslinux.Daemon")] +impl SupportedFunctions { + pub fn supported_functions(&self) -> &rog_platform::supported::SupportedFunctions { + &self.0 + } +} + +#[async_trait] +impl crate::ZbusRun for SupportedFunctions { + async fn add_to_server(self, server: &mut Connection) { + Self::add_to_server_helper(self, "/org/asuslinux/Supported", server).await; + } +} + +impl GetSupported for SupportedFunctions { + type A = SupportedFunctions; + + fn get_supported() -> Self::A { + Self(rog_platform::supported::SupportedFunctions { + anime_ctrl: CtrlAnime::get_supported(), + keyboard_led: CtrlKbdLed::get_supported(), + charge_ctrl: CtrlPower::get_supported(), + platform_profile: CtrlPlatformProfile::get_supported(), + rog_bios_ctrl: CtrlPlatform::get_supported(), + }) + } +} diff --git a/asusd/src/daemon.rs b/asusd/src/daemon.rs new file mode 100644 index 00000000..1bb7be62 --- /dev/null +++ b/asusd/src/daemon.rs @@ -0,0 +1,165 @@ +use std::env; +use std::error::Error; +use std::io::Write; +use std::sync::Arc; +use std::time::Duration; + +use ::zbus::export::futures_util::lock::Mutex; +use ::zbus::Connection; +use asusd::config::Config; +use asusd::ctrl_anime::config::AnimeConfig; +use asusd::ctrl_anime::trait_impls::CtrlAnimeZbus; +use asusd::ctrl_anime::CtrlAnime; +use asusd::ctrl_aura::controller::CtrlKbdLed; +use asusd::ctrl_aura::trait_impls::CtrlKbdLedZbus; +use asusd::ctrl_platform::CtrlPlatform; +use asusd::ctrl_power::CtrlPower; +use asusd::ctrl_profiles::config::ProfileConfig; +use asusd::ctrl_profiles::controller::CtrlPlatformProfile; +use asusd::ctrl_profiles::trait_impls::ProfileZbus; +use asusd::ctrl_supported::SupportedFunctions; +use asusd::{print_board_info, CtrlTask, GetSupported, Reloadable, ZbusRun}; +use config_traits::{StdConfig, StdConfigLoad, StdConfigLoad2}; +use log::{error, info, warn}; +use rog_aura::aura_detection::LaptopLedData; +use rog_dbus::DBUS_NAME; +use rog_profiles::Profile; +use tokio::time::sleep; +use zbus::SignalContext; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut logger = env_logger::Builder::new(); + logger + .parse_default_env() + .target(env_logger::Target::Stdout) + .format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args())) + .init(); + + let is_service = match env::var_os("IS_SERVICE") { + Some(val) => val == "1", + None => false, + }; + + if !is_service { + println!("asusd schould be only run from the right systemd service"); + println!( + "do not run in your terminal, if you need an logs please use journalctl -b -u asusd" + ); + println!("asusd will now exit"); + return Ok(()); + } + + info!(" daemon v{}", asusd::VERSION); + info!(" rog-anime v{}", rog_anime::VERSION); + info!(" rog-aura v{}", rog_aura::VERSION); + info!(" rog-dbus v{}", rog_dbus::VERSION); + info!(" rog-profiles v{}", rog_profiles::VERSION); + info!("rog-platform v{}", rog_platform::VERSION); + + start_daemon().await?; + Ok(()) +} + +/// The actual main loop for the daemon +async fn start_daemon() -> Result<(), Box> { + let supported = SupportedFunctions::get_supported(); + print_board_info(); + println!("{}", supported.supported_functions()); + + // Start zbus server + let mut connection = Connection::system().await?; + + let config = Config::new().load(); + let config = Arc::new(Mutex::new(config)); + + supported.add_to_server(&mut connection).await; + + match CtrlPlatform::new(config.clone()) { + Ok(ctrl) => { + let sig_ctx = CtrlPlatform::signal_context(&connection)?; + start_tasks(ctrl, &mut connection, sig_ctx).await?; + } + Err(err) => { + error!("CtrlPlatform: {}", err); + } + } + + match CtrlPower::new(config.clone()) { + Ok(ctrl) => { + let sig_ctx = CtrlPower::signal_context(&connection)?; + start_tasks(ctrl, &mut connection, sig_ctx).await?; + } + Err(err) => { + error!("CtrlPower: {}", err); + } + } + + if Profile::is_platform_profile_supported() { + let profile_config = ProfileConfig::new().load(); + match CtrlPlatformProfile::new(profile_config) { + Ok(ctrl) => { + let zbus = ProfileZbus(Arc::new(Mutex::new(ctrl))); + let sig_ctx = ProfileZbus::signal_context(&connection)?; + start_tasks(zbus, &mut connection, sig_ctx).await?; + } + Err(err) => { + error!("Profile control: {}", err); + } + } + } else { + warn!("platform_profile support not found"); + } + + match CtrlAnime::new(AnimeConfig::new().load()) { + Ok(ctrl) => { + let zbus = CtrlAnimeZbus(Arc::new(Mutex::new(ctrl))); + let sig_ctx = CtrlAnimeZbus::signal_context(&connection)?; + start_tasks(zbus, &mut connection, sig_ctx).await?; + } + Err(err) => { + info!("AniMe control: {}", err); + } + } + + let laptop = LaptopLedData::get_data(); + // CtrlKbdLed deviates from the config pattern above due to requiring a keyboard + // detection first + match CtrlKbdLed::new(laptop) { + Ok(ctrl) => { + let zbus = CtrlKbdLedZbus(Arc::new(Mutex::new(ctrl))); + let sig_ctx = CtrlKbdLedZbus::signal_context(&connection)?; + start_tasks(zbus, &mut connection, sig_ctx).await?; + } + Err(err) => { + error!("Keyboard control: {}", err); + } + } + + // Request dbus name after finishing initalizing all functions + connection.request_name(DBUS_NAME).await?; + + loop { + // This is just a blocker to idle and ensure the reator reacts + sleep(Duration::from_millis(1000)).await; + } +} + +async fn start_tasks( + mut zbus: T, + connection: &mut Connection, + signal_ctx: SignalContext<'static>, +) -> Result<(), Box> +where + T: ZbusRun + Reloadable + CtrlTask + Clone, +{ + let task = zbus.clone(); + + zbus.reload() + .await + .unwrap_or_else(|err| warn!("Controller error: {}", err)); + zbus.add_to_server(connection).await; + + task.create_tasks(signal_ctx).await.ok(); + Ok(()) +} diff --git a/asusd/src/error.rs b/asusd/src/error.rs new file mode 100644 index 00000000..64ef8a0f --- /dev/null +++ b/asusd/src/error.rs @@ -0,0 +1,135 @@ +use std::convert::From; +use std::fmt; + +use config_traits::ron; +use rog_anime::error::AnimeError; +use rog_platform::error::PlatformError; +use rog_profiles::error::ProfileError; + +#[derive(Debug)] +pub enum RogError { + ParseVendor, + ParseLed, + MissingProfile(String), + Udev(String, std::io::Error), + Path(String, std::io::Error), + Read(String, std::io::Error), + Write(String, std::io::Error), + NotSupported, + NotFound(String), + DoTask(String), + MissingFunction(String), + MissingLedBrightNode(String, std::io::Error), + ReloadFail(String), + Profiles(ProfileError), + Initramfs(String), + Modprobe(String), + Io(std::io::Error), + Zbus(zbus::Error), + ChargeLimit(u8), + AuraEffectNotSupported, + NoAuraKeyboard, + NoAuraNode, + Anime(AnimeError), + Platform(PlatformError), + SystemdUnitAction(String), + SystemdUnitWaitTimeout(String), + Command(String, std::io::Error), + ParseRon(ron::Error), +} + +impl fmt::Display for RogError { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RogError::ParseVendor => write!(f, "Parse gfx vendor error"), + RogError::ParseLed => write!(f, "Parse LED error"), + RogError::MissingProfile(profile) => write!(f, "Profile does not exist {}", profile), + RogError::Udev(deets, error) => write!(f, "udev {}: {}", deets, error), + RogError::Path(path, error) => write!(f, "Path {}: {}", path, error), + RogError::Read(path, error) => write!(f, "Read {}: {}", path, error), + RogError::Write(path, error) => write!(f, "Write {}: {}", path, error), + RogError::NotSupported => write!(f, "Not supported"), + RogError::NotFound(deets) => write!(f, "Not found: {}", deets), + RogError::DoTask(deets) => write!(f, "Task error: {}", deets), + RogError::MissingFunction(deets) => write!(f, "Missing functionality: {}", deets), + RogError::MissingLedBrightNode(path, error) => write!( + f, + "Led node at {} is missing, please check you have the required patch or dkms \ + module installed: {}", + path, error + ), + RogError::ReloadFail(deets) => write!(f, "Reload error: {}", deets), + 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::Io(detail) => write!(f, "std::io error: {}", detail), + RogError::Zbus(detail) => write!(f, "Zbus error: {}", detail), + RogError::ChargeLimit(value) => { + write!(f, "Invalid charging limit, not in range 20-100%: {}", value) + } + RogError::AuraEffectNotSupported => write!(f, "Aura effect not supported"), + RogError::NoAuraKeyboard => write!(f, "No supported Aura keyboard"), + RogError::NoAuraNode => write!(f, "No Aura keyboard node found"), + RogError::Anime(deets) => write!(f, "AniMe Matrix error: {}", deets), + RogError::Platform(deets) => write!(f, "Asus Platform error: {}", deets), + RogError::SystemdUnitAction(action) => { + write!(f, "systemd unit action {} failed", action) + } + RogError::SystemdUnitWaitTimeout(state) => { + write!( + f, + "Timed out waiting for systemd unit change {} state", + state + ) + } + RogError::Command(func, error) => write!(f, "Command exec error: {}: {}", func, error), + RogError::ParseRon(error) => write!(f, "Parse config error: {}", error), + } + } +} + +impl std::error::Error for RogError {} + +impl From for RogError { + fn from(err: ProfileError) -> Self { + RogError::Profiles(err) + } +} + +impl From for RogError { + fn from(err: AnimeError) -> Self { + RogError::Anime(err) + } +} + +impl From for RogError { + fn from(err: PlatformError) -> Self { + RogError::Platform(err) + } +} + +impl From for RogError { + fn from(err: zbus::Error) -> Self { + RogError::Zbus(err) + } +} + +impl From for RogError { + fn from(err: std::io::Error) -> Self { + RogError::Io(err) + } +} + +impl From for RogError { + fn from(err: ron::Error) -> Self { + RogError::ParseRon(err) + } +} + +impl From for zbus::fdo::Error { + #[inline] + fn from(err: RogError) -> Self { + zbus::fdo::Error::Failed(format!("{}", err)) + } +} diff --git a/asusd/src/lib.rs b/asusd/src/lib.rs new file mode 100644 index 00000000..1292e11f --- /dev/null +++ b/asusd/src/lib.rs @@ -0,0 +1,229 @@ +#![deny(unused_must_use)] +/// Configuration loading, saving +pub mod config; +/// Control of anime matrix display +pub mod ctrl_anime; +/// Keyboard LED brightness control, RGB, and LED display modes +pub mod ctrl_aura; +/// Control ASUS bios function such as boot sound, Optimus/Dedicated gfx mode +pub mod ctrl_platform; +/// Control of battery charge level +pub mod ctrl_power; +/// Control platform profiles + fan-curves if available +pub mod ctrl_profiles; + +/// Fetch all supported functions for the laptop +pub mod ctrl_supported; + +pub mod error; + +use std::future::Future; + +use async_trait::async_trait; +use log::{debug, info, warn}; +use logind_zbus::manager::ManagerProxy; +use zbus::export::futures_util::StreamExt; +use zbus::zvariant::ObjectPath; +use zbus::{Connection, SignalContext}; + +use crate::error::RogError; + +const CONFIG_PATH_BASE: &str = "/etc/asusd/"; + +/// This macro adds a function which spawns an `inotify` task on the passed in +/// `Executor`. +/// +/// The generated function is `watch_()`. Self requires the following +/// methods to be available: +/// - `() -> SomeValue`, functionally is a getter, but is allowed to have +/// side effects. +/// - `notify_(SignalContext, SomeValue)` +/// +/// In most cases if `SomeValue` is stored in a config then `()` getter is +/// expected to update it. The getter should *never* write back to the path or +/// attribute that is being watched or an infinite loop will occur. +/// +/// # Example +/// +/// ```ignore +/// impl CtrlRogBios { +/// task_watch_item!(panel_od platform); +/// task_watch_item!(gpu_mux_mode platform); +/// } +/// ``` +#[macro_export] +macro_rules! task_watch_item { + ($name:ident $self_inner:ident) => { + concat_idents::concat_idents!(fn_name = watch_, $name { + async fn fn_name( + &self, + signal_ctxt: SignalContext<'static>, + ) -> Result<(), RogError> { + use zbus::export::futures_util::StreamExt; + + let ctrl = self.clone(); + concat_idents::concat_idents!(watch_fn = monitor_, $name { + match self.$self_inner.watch_fn() { + Ok(watch) => { + tokio::spawn(async move { + let mut buffer = [0; 32]; + watch.into_event_stream(&mut buffer).unwrap().for_each(|_| async { + let value = ctrl.$name(); + concat_idents::concat_idents!(notif_fn = notify_, $name { + Self::notif_fn(&signal_ctxt, value).await.ok(); + }); + }).await; + }); + } + Err(e) => info!("inotify watch failed: {}. You can ignore this if your device does not support the feature", e), + } + }); + Ok(()) + } + }); + }; +} + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn print_board_info() { + let dmi = sysfs_class::DmiId::default(); + let board_name = dmi.board_name().expect("Could not get board_name"); + let prod_family = dmi.product_family().expect("Could not get product_family"); + + info!("Product family: {}", prod_family.trim()); + info!("Board name: {}", board_name.trim()); +} + +#[async_trait] +pub trait Reloadable { + async fn reload(&mut self) -> Result<(), RogError>; +} + +#[async_trait] +pub trait ZbusRun { + async fn add_to_server(self, server: &mut Connection); + + async fn add_to_server_helper( + iface: impl zbus::Interface, + path: &str, + server: &mut Connection, + ) { + server + .object_server() + .at(&ObjectPath::from_str_unchecked(path), iface) + .await + .map_err(|err| { + warn!("{}: add_to_server {}", path, err); + err + }) + .ok(); + } +} + +/// Set up a task to run on the async executor +#[async_trait] +pub trait CtrlTask { + fn zbus_path() -> &'static str; + + fn signal_context(connection: &Connection) -> Result, zbus::Error> { + SignalContext::new(connection, Self::zbus_path()) + } + + /// Implement to set up various tasks that may be required, using the + /// `Executor`. No blocking loops are allowed, or they must be run on a + /// separate thread. + async fn create_tasks(&self, signal: SignalContext<'static>) -> Result<(), RogError>; + + // /// Create a timed repeating task + // async fn repeating_task(&self, millis: u64, mut task: impl FnMut() + Send + + // 'static) { use std::time::Duration; + // use tokio::time; + // let mut timer = time::interval(Duration::from_millis(millis)); + // tokio::spawn(async move { + // timer.tick().await; + // task(); + // }); + // } + + /// Free helper method to create tasks to run on: sleep, wake, shutdown, + /// boot + /// + /// The closures can potentially block, so execution time should be the + /// minimal possible such as save a variable. + async fn create_sys_event_tasks< + Fut1, + Fut2, + Fut3, + Fut4, + F1: Send + 'static, + F2: Send + 'static, + F3: Send + 'static, + F4: Send + 'static, + >( + &self, + mut on_sleep: F1, + mut on_wake: F2, + mut on_shutdown: F3, + mut on_boot: F4, + ) where + F1: FnMut() -> Fut1, + F2: FnMut() -> Fut2, + F3: FnMut() -> Fut3, + F4: FnMut() -> Fut4, + Fut1: Future + Send, + Fut2: Future + Send, + Fut3: Future + Send, + Fut4: Future + Send, + { + let connection = Connection::system() + .await + .expect("Controller could not create dbus connection"); + + let manager = ManagerProxy::new(&connection) + .await + .expect("Controller could not create ManagerProxy"); + + tokio::spawn(async move { + if let Ok(mut notif) = manager.receive_prepare_for_sleep().await { + while let Some(event) = notif.next().await { + if let Ok(args) = event.args() { + if args.start { + debug!("Doing on_sleep()"); + on_sleep().await; + } else if !args.start() { + debug!("Doing on_wake()"); + on_wake().await; + } + } + } + } + }); + + let manager = ManagerProxy::new(&connection) + .await + .expect("Controller could not create ManagerProxy"); + + tokio::spawn(async move { + if let Ok(mut notif) = manager.receive_prepare_for_shutdown().await { + while let Some(event) = notif.next().await { + if let Ok(args) = event.args() { + if args.start { + debug!("Doing on_shutdown()"); + on_shutdown().await; + } else if !args.start() { + debug!("Doing on_boot()"); + on_boot().await; + } + } + } + } + }); + } +} + +pub trait GetSupported { + type A; + + fn get_supported() -> Self::A; +} diff --git a/simulators/src/animatrix/map_ga401.rs b/simulators/src/animatrix/map_ga401.rs new file mode 100644 index 00000000..feb77fae --- /dev/null +++ b/simulators/src/animatrix/map_ga401.rs @@ -0,0 +1,14 @@ +use super::Row; + +pub const GA401: [Row; 10] = [ + Row(0x01, 7, 34), + Row(0x01, 7 + 34, 34), + Row(0x01, 7 + 34 * 2, 34), + Row(0x01, 7 + 34 * 3, 34), + Row(0x01, 7 + 34 * 4, 34), + Row(0x01, 7 + 34 * 5, 34), + Row(0x01, 7 + 34 * 6, 34), + Row(0x01, 7 + 34 * 7, 34), + Row(0x01, 7 + 34 * 8, 34), + Row(0x01, 7 + 34 * 9, 34), +]; diff --git a/simulators/src/animatrix/map_ga402.rs b/simulators/src/animatrix/map_ga402.rs new file mode 100644 index 00000000..79b063ca --- /dev/null +++ b/simulators/src/animatrix/map_ga402.rs @@ -0,0 +1,15 @@ +use super::Row; + +pub const GA402: [Row; 11] = [ + Row(0x01, 7, 34), + Row(0x01, 7 + 34, 34), + Row(0x01, 7 + 34 * 2, 34), + Row(0x01, 7 + 34 * 3, 34), + Row(0x01, 7 + 34 * 4, 34), + Row(0x01, 7 + 34 * 5, 34), + Row(0x01, 7 + 34 * 6, 34), + Row(0x01, 7 + 34 * 7, 34), + Row(0x01, 7 + 34 * 8, 34), + Row(0x01, 7 + 34 * 9, 34), + Row(0x01, 7 + 34 * 10, 34), +]; diff --git a/simulators/src/animatrix/map_gu604.rs b/simulators/src/animatrix/map_gu604.rs new file mode 100644 index 00000000..6dfb7b2d --- /dev/null +++ b/simulators/src/animatrix/map_gu604.rs @@ -0,0 +1,14 @@ +use super::Row; + +pub const GU604: [Row; 10] = [ + Row(0x01, 7, 34), + Row(0x01, 7 + 34, 34), + Row(0x01, 7 + 34 * 2, 34), + Row(0x01, 7 + 34 * 3, 34), + Row(0x01, 7 + 34 * 4, 34), + Row(0x01, 7 + 34 * 5, 34), + Row(0x01, 7 + 34 * 6, 34), + Row(0x01, 7 + 34 * 7, 34), + Row(0x01, 7 + 34 * 8, 34), + Row(0x01, 7 + 34 * 9, 34), +]; diff --git a/simulators/src/animatrix/mod.rs b/simulators/src/animatrix/mod.rs new file mode 100644 index 00000000..e2a854c5 --- /dev/null +++ b/simulators/src/animatrix/mod.rs @@ -0,0 +1,75 @@ +use self::map_ga401::GA401; +use self::map_ga402::GA402; +use self::map_gu604::GU604; + +mod map_ga401; +mod map_ga402; +mod map_gu604; + +pub enum Model { + GA401, + GA402, + GU604, +} + +#[derive(Clone, Copy)] +pub struct Row( + /// The USB packet index, this is mapped to the 4th byte (idx = 3) and is + /// one of (in order of packets): 1. `0x01` + /// 2. `0x74` + /// 3. `0xe7` + pub u8, + /// Starting index in that packet + pub usize, + /// The length to read inclusive + pub usize, +); + +#[derive(Clone, Copy)] +pub struct LedShape { + /// Vertical offset from center for the top/bottom points + pub vertical: i32, + /// Horizontal offset from center for the top/bottom points + pub horizontal: i32, +} + +pub struct AniMatrix { + rows: Vec, + led_shape: LedShape, +} + +impl AniMatrix { + pub fn new(model: Model) -> Self { + let led_shape = match model { + Model::GA401 => LedShape { + vertical: 2, + horizontal: 3, + }, + Model::GA402 => LedShape { + vertical: 2, + horizontal: 3, + }, + Model::GU604 => LedShape { + vertical: 2, + horizontal: 3, + }, + }; + + // Do a hard mapping of each (derived from wireshardk captures) + let rows = match model { + Model::GA401 => GA401.to_vec(), + Model::GA402 => GA402.to_vec(), + Model::GU604 => GU604.to_vec(), + }; + + Self { rows, led_shape } + } + + pub fn rows(&self) -> &[Row] { + &self.rows + } + + pub fn led_shape(&self) -> LedShape { + self.led_shape + } +}