use crate::laptops::{LaptopLedData, ASUS_KEYBOARD_DEVICES}; use log::{error, warn}; use rog_aura::usb::{AuraDev1866, AuraDev19b6, AuraDevTuf, AuraDevice, AuraPowerDev}; use rog_aura::{AuraEffect, AuraModeNum, AuraZone, Direction, LedBrightness, Speed, GRADIENT}; use rog_platform::hid_raw::HidRaw; use rog_platform::keyboard_led::KeyboardLed; use serde_derive::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashSet}; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; pub static AURA_CONFIG_PATH: &str = "/etc/asusd/aura.conf"; /// 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), AuraDev1866(HashSet), AuraDev19b6(HashSet), } impl AuraPowerConfig { /// Invalid for TUF laptops pub fn to_bytes(control: &Self) -> [u8; 3] { match control { AuraPowerConfig::AuraDevTuf(_) => [0, 0, 0], AuraPowerConfig::AuraDev1866(c) => { let c: Vec = c.iter().copied().collect(); AuraDev1866::to_bytes(&c) } AuraPowerConfig::AuraDev19b6(c) => { let c: Vec = c.iter().copied().collect(); AuraDev19b6::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::AuraDev1866(c) = control { return Some([ true, c.contains(&AuraDev1866::Boot), c.contains(&AuraDev1866::Awake), c.contains(&AuraDev1866::Sleep), c.contains(&AuraDev1866::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: AuraDev1866, on: bool) { if let Self::AuraDev1866(p) = self { if on { p.insert(power); } else { p.remove(&power); } } } pub fn set_0x19b6(&mut self, power: AuraDev19b6, on: bool) { if let Self::AuraDev19b6(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::AuraDev1866(d) => AuraPowerDev { tuf: vec![], x1866: d.iter().copied().collect(), x19b6: vec![], }, AuraPowerConfig::AuraDev19b6(d) => AuraPowerDev { tuf: vec![], x1866: vec![], x19b6: d.iter().copied().collect(), }, } } } #[derive(Deserialize, Serialize)] // #[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 Default for AuraConfig { fn default() -> Self { let mut prod_id = AuraDevice::Unknown; for prod in ASUS_KEYBOARD_DEVICES.iter() { if HidRaw::new(prod).is_ok() { prod_id = AuraDevice::from(*prod); break; } } if prod_id == AuraDevice::Unknown { if let Ok(p) = KeyboardLed::new() { if p.has_kbd_rgb_mode() { prod_id = AuraDevice::Tuf; } } } let enabled = if prod_id == AuraDevice::X19B6 { AuraPowerConfig::AuraDev19b6(HashSet::from([ AuraDev19b6::BootLogo, AuraDev19b6::BootKeyb, AuraDev19b6::SleepLogo, AuraDev19b6::SleepKeyb, AuraDev19b6::AwakeLogo, AuraDev19b6::AwakeKeyb, AuraDev19b6::ShutdownLogo, AuraDev19b6::ShutdownKeyb, AuraDev19b6::BootBar, AuraDev19b6::AwakeBar, AuraDev19b6::SleepBar, AuraDev19b6::ShutdownBar, ])) } else if prod_id == AuraDevice::Tuf { AuraPowerConfig::AuraDevTuf(HashSet::from([ AuraDevTuf::Awake, AuraDevTuf::Boot, AuraDevTuf::Sleep, AuraDevTuf::Keyboard, ])) } else { AuraPowerConfig::AuraDev1866(HashSet::from([ AuraDev1866::Awake, AuraDev1866::Boot, AuraDev1866::Sleep, AuraDev1866::Keyboard, AuraDev1866::Lightbar, ])) }; AuraConfig { brightness: LedBrightness::Med, current_mode: AuraModeNum::Static, builtins: BTreeMap::new(), multizone: None, multizone_on: false, enabled, } } } impl AuraConfig { /// `load` will attempt to read the config, and panic if the dir is missing pub fn load(supported_led_modes: &LaptopLedData) -> Self { let mut file = OpenOptions::new() .read(true) .write(true) .create(true) .open(&AURA_CONFIG_PATH) .unwrap_or_else(|_| { panic!( "The file {} or directory /etc/asusd/ is missing", AURA_CONFIG_PATH ) }); // okay to cause panic here let mut buf = String::new(); if let Ok(read_len) = file.read_to_string(&mut buf) { if read_len == 0 { return AuraConfig::create_default(&mut file, supported_led_modes); } else { if let Ok(data) = serde_json::from_str(&buf) { return data; } warn!( "Could not deserialise {}.\nWill rename to {}-old and recreate config", AURA_CONFIG_PATH, AURA_CONFIG_PATH ); let cfg_old = AURA_CONFIG_PATH.to_string() + "-old"; std::fs::rename(AURA_CONFIG_PATH, cfg_old).unwrap_or_else(|err| { panic!( "Could not rename. Please remove {} then restart service: Error {}", AURA_CONFIG_PATH, err ) }); } } AuraConfig::create_default(&mut file, supported_led_modes) } fn create_default(file: &mut File, support_data: &LaptopLedData) -> Self { // create a default config here let mut config = AuraConfig::default(); for n in &support_data.standard { config .builtins .insert(*n, AuraEffect::default_with_mode(*n)); if !support_data.multizone.is_empty() { let mut default = vec![]; for (i, tmp) in support_data.multizone.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); } } } // Should be okay to unwrap this as is since it is a Default let json = serde_json::to_string(&config).unwrap(); file.write_all(json.as_bytes()) .unwrap_or_else(|_| panic!("Could not write {}", AURA_CONFIG_PATH)); config } pub fn read(&mut self) { let mut file = OpenOptions::new() .read(true) .open(&AURA_CONFIG_PATH) .unwrap_or_else(|err| panic!("Error reading {}: {}", AURA_CONFIG_PATH, err)); let mut buf = String::new(); if let Ok(l) = file.read_to_string(&mut buf) { if l == 0 { warn!("File is empty {}", AURA_CONFIG_PATH); } else { let x = serde_json::from_str(&buf) .unwrap_or_else(|_| panic!("Could not deserialise {}", AURA_CONFIG_PATH)); *self = x; } } } pub fn write(&self) { let mut file = File::create(AURA_CONFIG_PATH).expect("Couldn't overwrite config"); let json = serde_json::to_string_pretty(self).expect("Parse config to JSON failed"); file.write_all(json.as_bytes()) .unwrap_or_else(|err| error!("Could not write config: {}", err)); } /// 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; match effect.zone() { AuraZone::None => { self.builtins.insert(*effect.mode(), effect); self.multizone_on = false; } _ => { 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 super::AuraConfig; use rog_aura::{AuraEffect, AuraModeNum, AuraZone, Colour}; #[test] fn set_multizone_4key_config() { let mut config = AuraConfig::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::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); } }