Complete rename

This commit is contained in:
Luke D Jones
2020-08-04 20:05:12 +12:00
parent 76a8f7678c
commit 584ca023ad
50 changed files with 150 additions and 307 deletions

120
asus-nb-ctrl/src/config.rs Normal file
View File

@@ -0,0 +1,120 @@
use asus_nb::aura_modes::AuraModes;
use serde_derive::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
pub static CONFIG_PATH: &str = "/etc/asusd.conf";
#[derive(Default, Deserialize, Serialize)]
pub struct Config {
pub fan_mode: u8,
pub bat_charge_limit: u8,
pub brightness: u8,
pub current_mode: u8,
pub builtin_modes: Vec<AuraModes>,
pub mode_performance: FanModeSettings,
}
impl Config {
/// `load` will attempt to read the config, but if it is not found it
/// will create a new default config and write that out.
pub fn load(mut self, supported_led_modes: &[u8]) -> Self {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&CONFIG_PATH)
.expect("config file error");
let mut buf = String::new();
if let Ok(l) = file.read_to_string(&mut buf) {
if l == 0 {
// create a default config here
let mut c = Config::default();
c.bat_charge_limit = 100;
c.current_mode = 0;
for n in supported_led_modes {
c.builtin_modes.push(AuraModes::from(*n))
}
// Should be okay to unwrap this as is since it is a Default
let json = serde_json::to_string_pretty(&c).unwrap();
file.write_all(json.as_bytes())
.unwrap_or_else(|_| panic!("Could not deserialise {}", CONFIG_PATH));
self = c;
} else {
self = serde_json::from_str(&buf)
.unwrap_or_else(|_| panic!("Could not deserialise {}", CONFIG_PATH));
}
}
self
}
pub fn read(&mut self) {
let mut file = OpenOptions::new()
.read(true)
.open(&CONFIG_PATH)
.expect("config file error");
let mut buf = String::new();
if let Ok(l) = file.read_to_string(&mut buf) {
if l == 0 {
panic!("Missing {}", CONFIG_PATH);
} else {
let x: Config = serde_json::from_str(&buf)
.unwrap_or_else(|_| panic!("Could not deserialise {}", CONFIG_PATH));
*self = x;
}
}
}
pub fn write(&self) {
let mut file = File::create(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())
.expect("Saving config failed");
}
pub fn set_mode_data(&mut self, mode: AuraModes) {
let byte: u8 = (&mode).into();
for (index, n) in self.builtin_modes.iter().enumerate() {
if byte == u8::from(n) {
// Consume it, OMNOMNOMNOM
self.builtin_modes[index] = mode;
break;
}
}
}
pub fn get_led_mode_data(&self, num: u8) -> Option<&AuraModes> {
for mode in &self.builtin_modes {
if u8::from(mode) == num {
return Some(mode);
}
}
None
}
}
#[derive(Default, Deserialize, Serialize)]
pub struct FanModeSettings {
pub normal: IntelPState,
pub boost: IntelPState,
pub silent: IntelPState,
}
#[derive(Deserialize, Serialize)]
pub struct IntelPState {
pub min_percentage: u8,
pub max_percentage: u8,
pub no_turbo: bool,
}
impl Default for IntelPState {
fn default() -> Self {
IntelPState {
min_percentage: 0,
max_percentage: 100,
no_turbo: false,
}
}
}

View File

@@ -0,0 +1,225 @@
const INIT_STR: &str = "ASUS Tech.Inc.";
const PACKET_SIZE: usize = 640;
// Only these two packets must be 17 bytes
const DEV_PAGE: u8 = 0x5e;
// These bytes are in [1] position of the array
const WRITE: u8 = 0xc0;
const INIT: u8 = 0xc2;
const APPLY: u8 = 0xc3;
const SET: u8 = 0xc4;
use crate::config::Config;
use log::{error, info, warn};
use asus_nb::error::AuraError;
use rusb::{Device, DeviceHandle};
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
#[allow(dead_code)]
#[derive(Debug)]
pub enum AnimatrixCommand {
Apply,
Set,
WriteImage(Vec<Vec<u8>>),
//ReloadLast,
}
pub struct CtrlAnimeDisplay {
handle: DeviceHandle<rusb::GlobalContext>,
initialised: bool,
}
use ::dbus::{nonblock::SyncConnection, tree::Signal};
use async_trait::async_trait;
#[async_trait]
impl crate::Controller for CtrlAnimeDisplay {
type A = Vec<Vec<u8>>;
/// Spawns two tasks which continuously check for changes
fn spawn_task(
mut self,
_: Arc<Mutex<Config>>,
mut recv: Receiver<Self::A>,
_: Option<Arc<SyncConnection>>,
_: Option<Arc<Signal<()>>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(image) = recv.recv().await {
self.do_command(AnimatrixCommand::WriteImage(image))
.await
.unwrap_or_else(|err| warn!("{}", err));
}
})
}
async fn reload_from_config(&mut self, _: &mut Config) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
impl CtrlAnimeDisplay {
#[inline]
pub fn new() -> Result<CtrlAnimeDisplay, Box<dyn Error>> {
// We don't expect this ID to ever change
let device = CtrlAnimeDisplay::get_device(0x0b05, 0x193b).map_err(|err| {
warn!("Could not get AniMe display handle: {:?}", err);
err
})?;
let mut device = device.open()?;
device.reset()?;
device.set_auto_detach_kernel_driver(true).map_err(|err| {
error!("Auto-detach kernel driver failed: {:?}", err);
err
})?;
device.claim_interface(0).map_err(|err| {
error!("Could not claim device interface: {:?}", err);
err
})?;
info!("Device has an AniMe Matrix display");
Ok(CtrlAnimeDisplay {
handle: device,
initialised: false,
})
}
#[inline]
fn get_device(vendor: u16, product: u16) -> Result<Device<rusb::GlobalContext>, rusb::Error> {
for device in rusb::devices()?.iter() {
let device_desc = device.device_descriptor()?;
if device_desc.vendor_id() == vendor && device_desc.product_id() == product {
return Ok(device);
}
}
Err(rusb::Error::NoDevice)
}
pub async fn do_command(&mut self, command: AnimatrixCommand) -> Result<(), AuraError> {
if !self.initialised {
self.do_initialization().await?
}
match command {
AnimatrixCommand::WriteImage(effect) => self.write_image(effect).await?,
AnimatrixCommand::Set => self.do_set().await?,
AnimatrixCommand::Apply => self.do_apply().await?,
//AnimatrixCommand::ReloadLast => self.reload_last_builtin(&config).await?,
}
Ok(())
}
/// Should only be used if the bytes you are writing are verified correct
#[inline]
async fn write_bytes(&self, message: &[u8]) -> Result<(), AuraError> {
let prev = std::time::Instant::now();
match self.handle.write_control(
0x21, // request_type
0x09, // request
0x35e, // value
0x00, // index
message,
Duration::from_millis(200),
) {
Ok(_) => {
println!(
"{:?}",
std::time::Instant::now().duration_since(prev).as_micros()
);
}
Err(err) => match err {
rusb::Error::Timeout => {}
_ => error!("Failed to write to led interrupt: {:?}", err),
},
}
Ok(())
}
/// Write an Animatrix image
///
/// The expected input here is *two* Vectors, 640 bytes in length. The two vectors
/// are each one half of the full image write.
///
/// After each write a flush is written, it is assumed that this tells the device to
/// go ahead and display the written bytes
///
/// # Note:
/// The vectors are expected to contain the full sequence of bytes as follows
///
/// - Write pane 1: 0x5e 0xc0 0x02 0x01 0x00 0x73 0x02 .. <led brightness>
/// - Write pane 2: 0x5e 0xc0 0x02 0x74 0x02 0x73 0x02 .. <led brightness>
///
/// Where led brightness is 0..255, low to high
#[inline]
async fn write_image(&mut self, image: Vec<Vec<u8>>) -> Result<(), AuraError> {
for row in image.iter() {
self.write_bytes(row).await?;
}
self.do_flush().await?;
Ok(())
}
#[inline]
async fn do_initialization(&mut self) -> Result<(), AuraError> {
let mut init = [0; PACKET_SIZE];
init[0] = DEV_PAGE; // This is the USB page we're using throughout
for (idx, byte) in INIT_STR.as_bytes().iter().enumerate() {
init[idx + 1] = *byte
}
self.write_bytes(&init).await?;
// clear the init array and write other init message
for ch in init.iter_mut() {
*ch = 0;
}
init[0] = DEV_PAGE; // write it to be sure?
init[1] = INIT;
self.write_bytes(&init).await?;
self.initialised = true;
Ok(())
}
#[inline]
async fn do_flush(&mut self) -> Result<(), AuraError> {
let mut flush = [0; PACKET_SIZE];
flush[0] = DEV_PAGE;
flush[1] = WRITE;
flush[2] = 0x03;
self.write_bytes(&flush).await?;
Ok(())
}
#[inline]
async fn do_set(&mut self) -> Result<(), AuraError> {
let mut flush = [0; PACKET_SIZE];
flush[0] = DEV_PAGE;
flush[1] = SET;
flush[2] = 0x01;
flush[3] = 0x80;
self.write_bytes(&flush).await?;
Ok(())
}
#[inline]
async fn do_apply(&mut self) -> Result<(), AuraError> {
let mut flush = [0; PACKET_SIZE];
flush[0] = DEV_PAGE;
flush[1] = APPLY;
flush[2] = 0x01;
flush[3] = 0x80;
self.write_bytes(&flush).await?;
Ok(())
}
}

View File

@@ -0,0 +1,120 @@
use crate::config::Config;
use log::{error, info, warn};
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
static BAT_CHARGE_PATH: &str = "/sys/class/power_supply/BAT0/charge_control_end_threshold";
pub struct CtrlCharge {
path: &'static str,
}
use ::dbus::{nonblock::SyncConnection, tree::Signal};
use async_trait::async_trait;
#[async_trait]
impl crate::Controller for CtrlCharge {
type A = u8;
/// Spawns two tasks which continuously check for changes
fn spawn_task(
self,
config: Arc<Mutex<Config>>,
mut recv: Receiver<Self::A>,
_: Option<Arc<SyncConnection>>,
_: Option<Arc<Signal<()>>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(n) = recv.recv().await {
let mut config = config.lock().await;
self.set_charge_limit(n, &mut config)
.unwrap_or_else(|err| warn!("{:?}", err));
}
})
}
async fn reload_from_config(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>> {
config.read();
info!("Reloaded battery charge limit");
self.set_charge_limit(config.bat_charge_limit, config)
}
}
impl CtrlCharge {
pub fn new() -> Result<Self, Box<dyn Error>> {
let path = CtrlCharge::get_battery_path()?;
info!("Device has battery charge threshold control");
Ok(CtrlCharge { path })
}
// /// Spawns two tasks which continuously check for changes
// pub(crate) fn spawn_task(
// self,
// config: Arc<Mutex<Config>>,
// mut recv: Receiver<u8>,
// ) -> JoinHandle<()> {
// tokio::spawn(async move {
// while let Some(n) = recv.recv().await {
// let mut config = config.lock().await;
// self
// .set_charge_limit(n, &mut config)
// .unwrap_or_else(|err| warn!("{:?}", err));
// }
// })
// }
fn get_battery_path() -> Result<&'static str, std::io::Error> {
if Path::new(BAT_CHARGE_PATH).exists() {
Ok(BAT_CHARGE_PATH)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Charge control not available",
))
}
}
// pub(super) fn reload_from_config(
// &self,
// config: &mut Config,
// ) -> Result<(), Box<dyn Error>> {
// config.read();
// info!("Reloaded battery charge limit");
// self.set_charge_limit(config.bat_charge_limit, config)
// }
pub(super) fn set_charge_limit(
&self,
limit: u8,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
if limit < 20 || limit > 100 {
warn!(
"Unable to set battery charge limit, must be between 20-100: requested {}",
limit
);
}
let mut file = OpenOptions::new()
.write(true)
.open(self.path)
.map_err(|err| {
warn!("Failed to open battery charge limit path: {:?}", err);
err
})?;
file.write_all(limit.to_string().as_bytes())
.unwrap_or_else(|err| error!("Could not write to {}, {:?}", BAT_CHARGE_PATH, err));
info!("Battery charge limit: {}", limit);
config.bat_charge_limit = limit;
config.write();
Ok(())
}
}

View File

@@ -0,0 +1,269 @@
use crate::config::Config;
use log::{error, info, warn};
use std::error::Error;
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
static FAN_TYPE_1_PATH: &str = "/sys/devices/platform/asus-nb-wmi/throttle_thermal_policy";
static FAN_TYPE_2_PATH: &str = "/sys/devices/platform/asus-nb-wmi/fan_boost_mode";
static AMD_BOOST_PATH: &str = "/sys/devices/system/cpu/cpufreq/boost";
pub struct CtrlFanAndCPU {
path: &'static str,
}
use ::dbus::{nonblock::SyncConnection, tree::Signal};
use async_trait::async_trait;
#[async_trait]
impl crate::Controller for CtrlFanAndCPU {
type A = u8;
/// Spawns two tasks which continuously check for changes
fn spawn_task(
self,
config: Arc<Mutex<Config>>,
mut recv: Receiver<Self::A>,
_: Option<Arc<SyncConnection>>,
_: Option<Arc<Signal<()>>>,
) -> JoinHandle<()> {
let gate1 = Arc::new(Mutex::new(self));
let gate2 = gate1.clone();
let config1 = config.clone();
// spawn an endless loop
tokio::spawn(async move {
while let Some(mode) = recv.recv().await {
let mut config = config1.lock().await;
if let Ok(mut lock) = gate1.try_lock() {
lock.set_fan_mode(mode, &mut config)
.unwrap_or_else(|err| warn!("{:?}", err));
}
}
});
// need to watch file path
tokio::spawn(async move {
loop {
if let Ok(mut lock) = gate2.try_lock() {
let mut config = config.lock().await;
lock.fan_mode_check_change(&mut config)
.unwrap_or_else(|err| warn!("{:?}", err));
}
tokio::time::delay_for(std::time::Duration::from_millis(500)).await;
}
})
}
async fn reload_from_config(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>> {
let mut file = OpenOptions::new().write(true).open(self.path)?;
file.write_all(format!("{:?}\n", config.fan_mode).as_bytes())
.unwrap_or_else(|err| error!("Could not write to {}, {:?}", self.path, err));
self.set_pstate_for_fan_mode(FanLevel::from(config.fan_mode), config)?;
info!("Reloaded fan mode: {:?}", FanLevel::from(config.fan_mode));
Ok(())
}
}
impl CtrlFanAndCPU {
pub fn new() -> Result<Self, Box<dyn Error>> {
let path = CtrlFanAndCPU::get_fan_path()?;
info!("Device has thermal throttle control");
Ok(CtrlFanAndCPU { path })
}
fn get_fan_path() -> Result<&'static str, std::io::Error> {
if Path::new(FAN_TYPE_1_PATH).exists() {
Ok(FAN_TYPE_1_PATH)
} else if Path::new(FAN_TYPE_2_PATH).exists() {
Ok(FAN_TYPE_2_PATH)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Fan mode not available",
))
}
}
pub(super) fn fan_mode_check_change(
&mut self,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
let mut file = OpenOptions::new().read(true).open(self.path)?;
let mut buf = [0u8; 1];
file.read_exact(&mut buf)?;
if let Some(num) = char::from(buf[0]).to_digit(10) {
if config.fan_mode != num as u8 {
config.fan_mode = num as u8;
config.write();
self.set_pstate_for_fan_mode(FanLevel::from(config.fan_mode), config)?;
info!(
"Fan mode was changed: {:?}",
FanLevel::from(config.fan_mode)
);
}
return Ok(());
}
let err = std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Fan-level could not be parsed",
);
Err(Box::new(err))
}
pub(super) fn set_fan_mode(
&mut self,
n: u8,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
let mut fan_ctrl = OpenOptions::new().write(true).open(self.path)?;
config.fan_mode = n;
config.write();
fan_ctrl
.write_all(format!("{:?}\n", config.fan_mode).as_bytes())
.unwrap_or_else(|err| error!("Could not write to {}, {:?}", self.path, err));
info!("Fan mode set to: {:?}", FanLevel::from(config.fan_mode));
self.set_pstate_for_fan_mode(FanLevel::from(n), config)?;
Ok(())
}
fn set_pstate_for_fan_mode(
&self,
mode: FanLevel,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
// Set CPU pstate
if let Ok(pstate) = intel_pstate::PState::new() {
match mode {
FanLevel::Normal => {
pstate.set_min_perf_pct(config.mode_performance.normal.min_percentage)?;
pstate.set_max_perf_pct(config.mode_performance.normal.max_percentage)?;
pstate.set_no_turbo(config.mode_performance.normal.no_turbo)?;
info!(
"Intel CPU Power: min: {:?}%, max: {:?}%, turbo: {:?}",
config.mode_performance.normal.min_percentage,
config.mode_performance.normal.max_percentage,
!config.mode_performance.normal.no_turbo
);
}
FanLevel::Boost => {
pstate.set_min_perf_pct(config.mode_performance.boost.min_percentage)?;
pstate.set_max_perf_pct(config.mode_performance.boost.max_percentage)?;
pstate.set_no_turbo(config.mode_performance.boost.no_turbo)?;
info!(
"Intel CPU Power: min: {:?}%, max: {:?}%, turbo: {:?}",
config.mode_performance.boost.min_percentage,
config.mode_performance.boost.max_percentage,
!config.mode_performance.boost.no_turbo
);
}
FanLevel::Silent => {
pstate.set_min_perf_pct(config.mode_performance.silent.min_percentage)?;
pstate.set_max_perf_pct(config.mode_performance.silent.max_percentage)?;
pstate.set_no_turbo(config.mode_performance.silent.no_turbo)?;
info!(
"Intel CPU Power: min: {:?}%, max: {:?}%, turbo: {:?}",
config.mode_performance.silent.min_percentage,
config.mode_performance.silent.max_percentage,
!config.mode_performance.silent.no_turbo
);
}
}
} else {
info!("Setting pstate for AMD CPU");
// must be AMD CPU
let mut file = OpenOptions::new()
.write(true)
.open(AMD_BOOST_PATH)
.map_err(|err| {
warn!("Failed to open AMD boost: {:?}", err);
err
})?;
match mode {
FanLevel::Normal => {
let boost = if config.mode_performance.normal.no_turbo {
"0"
} else {
"1"
}; // opposite of Intel
file.write_all(boost.as_bytes()).unwrap_or_else(|err| {
error!("Could not write to {}, {:?}", AMD_BOOST_PATH, err)
});
info!("AMD CPU Turbo: {:?}", boost);
}
FanLevel::Boost => {
let boost = if config.mode_performance.boost.no_turbo {
"0"
} else {
"1"
};
file.write_all(boost.as_bytes()).unwrap_or_else(|err| {
error!("Could not write to {}, {:?}", AMD_BOOST_PATH, err)
});
info!("AMD CPU Turbo: {:?}", boost);
}
FanLevel::Silent => {
let boost = if config.mode_performance.silent.no_turbo {
"0"
} else {
"1"
};
file.write_all(boost.as_bytes()).unwrap_or_else(|err| {
error!("Could not write to {}, {:?}", AMD_BOOST_PATH, err)
});
info!("AMD CPU Turbo: {:?}", boost);
}
}
}
Ok(())
}
}
use crate::error::RogError;
#[derive(Debug)]
pub enum FanLevel {
Normal,
Boost,
Silent,
}
impl FromStr for FanLevel {
type Err = RogError;
fn from_str(s: &str) -> Result<Self, RogError> {
match s.to_lowercase().as_str() {
"normal" => Ok(FanLevel::Normal),
"boost" => Ok(FanLevel::Boost),
"silent" => Ok(FanLevel::Silent),
_ => Err(RogError::ParseFanLevel),
}
}
}
impl From<u8> for FanLevel {
fn from(n: u8) -> Self {
match n {
0 => FanLevel::Normal,
1 => FanLevel::Boost,
2 => FanLevel::Silent,
_ => FanLevel::Normal,
}
}
}
impl From<FanLevel> for u8 {
fn from(n: FanLevel) -> Self {
match n {
FanLevel::Normal => 0,
FanLevel::Boost => 1,
FanLevel::Silent => 2,
}
}
}

View File

@@ -0,0 +1,253 @@
// Only these two packets must be 17 bytes
static LED_APPLY: [u8; 17] = [0x5d, 0xb4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
static LED_SET: [u8; 17] = [0x5d, 0xb5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
use crate::{config::Config, error::RogError};
use dbus::{channel::Sender, nonblock::SyncConnection, tree::Signal};
use log::{info, warn};
use asus_nb::{
aura_brightness_bytes, aura_modes::AuraModes, fancy::KeyColourArray, DBUS_IFACE, DBUS_PATH,
LED_MSG_LEN,
};
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
pub struct CtrlKbdBacklight {
dev_node: String,
supported_modes: Vec<u8>,
flip_effect_write: bool,
}
use async_trait::async_trait;
#[async_trait]
impl crate::Controller for CtrlKbdBacklight {
type A = AuraModes;
/// Spawns two tasks which continuously check for changes
fn spawn_task(
mut self,
config: Arc<Mutex<Config>>,
mut recv: Receiver<Self::A>,
connection: Option<Arc<SyncConnection>>,
signal: Option<Arc<Signal<()>>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(command) = recv.recv().await {
let mut config = config.lock().await;
match &command {
AuraModes::RGB(_) => {
self.do_command(command, &mut config)
.await
.unwrap_or_else(|err| warn!("{}", err));
}
_ => {
let json = serde_json::to_string(&command).unwrap();
self.do_command(command, &mut config)
.await
.unwrap_or_else(|err| warn!("{}", err));
connection
.as_ref()
.expect("LED Controller must have DBUS connection")
.send(
signal
.as_ref()
.expect("LED Controller must have DBUS signal")
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(json),
)
.unwrap_or_else(|_| 0);
}
}
}
})
}
async fn reload_from_config(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>> {
// set current mode (if any)
if self.supported_modes.len() > 1 {
if self.supported_modes.contains(&config.current_mode) {
let mode = config
.get_led_mode_data(config.current_mode)
.ok_or(RogError::NotSupported)?
.to_owned();
self.write_mode(&mode).await?;
info!("Reloaded last used mode");
} else {
warn!(
"An unsupported mode was set: {}, reset to first mode available",
<&str>::from(&<AuraModes>::from(config.current_mode))
);
for (idx, mode) in config.builtin_modes.iter_mut().enumerate() {
if !self.supported_modes.contains(&mode.into()) {
config.builtin_modes.remove(idx);
config.write();
break;
}
}
config.current_mode = self.supported_modes[0];
// TODO: do a recursive call with a boxed dyn future later
let mode = config
.get_led_mode_data(config.current_mode)
.ok_or(RogError::NotSupported)?
.to_owned();
self.write_mode(&mode).await?;
info!("Reloaded last used mode");
}
}
// Reload brightness
let bright = config.brightness;
let bytes = aura_brightness_bytes(bright);
self.write_bytes(&bytes).await?;
info!("Reloaded last used brightness");
Ok(())
}
}
impl CtrlKbdBacklight {
#[inline]
pub fn new(id_product: &str, supported_modes: Vec<u8>) -> Result<Self, std::io::Error> {
let mut enumerator = udev::Enumerator::new()?;
enumerator.match_subsystem("hidraw")?;
for device in enumerator.scan_devices()? {
if let Some(parent) = device.parent_with_subsystem_devtype("usb", "usb_device")? {
if parent.attribute_value("idProduct").unwrap() == id_product
// && device.parent().unwrap().sysnum().unwrap() == 3
{
if let Some(dev_node) = device.devnode() {
info!("Device has keyboard backlight control");
info!("Using device at: {:?} for LED control", dev_node);
return Ok(CtrlKbdBacklight {
dev_node: dev_node.to_string_lossy().to_string(),
supported_modes,
flip_effect_write: false,
});
}
}
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Device node not found",
))
}
pub async fn do_command(
&mut self,
mode: AuraModes,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
self.set_and_save(mode, config).await
}
/// Should only be used if the bytes you are writing are verified correct
#[inline]
async fn write_bytes(&self, message: &[u8]) -> Result<(), Box<dyn Error>> {
if let Ok(mut file) = OpenOptions::new().write(true).open(&self.dev_node) {
file.write_all(message).unwrap();
return Ok(());
}
Err(Box::new(RogError::NotSupported))
}
/// Write an effect block
#[inline]
async fn write_effect(&mut self, effect: &[Vec<u8>]) -> Result<(), Box<dyn Error>> {
if self.flip_effect_write {
for row in effect.iter().rev() {
self.write_bytes(row).await?;
}
} else {
for row in effect.iter() {
self.write_bytes(row).await?;
}
}
self.flip_effect_write = !self.flip_effect_write;
Ok(())
}
/// Used to set a builtin mode and save the settings for it
///
/// This needs to be universal so that settings applied by dbus stick
#[inline]
async fn set_and_save(
&mut self,
mode: AuraModes,
config: &mut Config,
) -> Result<(), Box<dyn Error>> {
match mode {
AuraModes::LedBrightness(n) => {
let bytes: [u8; LED_MSG_LEN] = (&mode).into();
self.write_bytes(&bytes).await?;
config.brightness = n;
config.write();
info!("LED brightness set to {:#?}", n);
}
AuraModes::RGB(v) => {
if v.is_empty() || v[0].is_empty() {
let bytes = KeyColourArray::get_init_msg();
self.write_bytes(&bytes).await?;
} else {
self.write_effect(&v).await?;
}
}
_ => {
let mode_num: u8 = u8::from(&mode);
self.write_mode(&mode).await?;
config.current_mode = mode_num;
config.set_mode_data(mode);
config.write();
}
}
Ok(())
}
#[inline]
async fn write_mode(&mut self, mode: &AuraModes) -> Result<(), Box<dyn Error>> {
match mode {
AuraModes::RGB(v) => {
if v.is_empty() || v[0].is_empty() {
let bytes = KeyColourArray::get_init_msg();
self.write_bytes(&bytes).await?;
} else {
self.write_effect(v).await?;
}
}
_ => {
let mode_num: u8 = u8::from(mode);
match mode {
AuraModes::MultiStatic(_) => {
if self.supported_modes.contains(&mode_num) {
let bytes: [[u8; LED_MSG_LEN]; 4] = mode.into();
for array in bytes.iter() {
self.write_bytes(array).await?;
}
}
}
_ => {
if self.supported_modes.contains(&mode_num) {
let bytes: [u8; LED_MSG_LEN] = mode.into();
self.write_bytes(&bytes).await?;
}
}
}
self.write_bytes(&LED_SET).await?;
// Changes won't persist unless apply is set
self.write_bytes(&LED_APPLY).await?;
}
}
Ok(())
}
// #[inline]
// pub async fn reload_from_config(&mut self, config: &mut Config) -> Result<(), RogError> {
// }
}

240
asus-nb-ctrl/src/daemon.rs Normal file
View File

@@ -0,0 +1,240 @@
use daemon::{
config::Config, ctrl_anime::CtrlAnimeDisplay, ctrl_charge::CtrlCharge, ctrl_fan_cpu::CtrlFanAndCPU,
ctrl_leds::CtrlKbdBacklight, dbus::dbus_create_tree, laptops::match_laptop,
};
use dbus::{channel::Sender, nonblock::SyncConnection, tree::Signal};
use daemon::Controller;
use dbus_tokio::connection;
use log::{error, warn, info};
use asus_nb::{DBUS_IFACE, DBUS_NAME, DBUS_PATH};
use std::error::Error;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::io::Write;
use log::LevelFilter;
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut logger = env_logger::Builder::new();
logger
.target(env_logger::Target::Stdout)
.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()))
.filter(None, LevelFilter::Info)
.init();
info!("Version: {}", daemon::VERSION);
start_daemon().await?;
Ok(())
}
// Timing is such that:
// - interrupt write is minimum 1ms (sometimes lower)
// - read interrupt must timeout, minimum of 1ms
// - for a single usb packet, 2ms total.
// - to maintain constant times of 1ms, per-key colours should use
// the effect endpoint so that the complete colour block is written
// as fast as 1ms per row of the matrix inside it. (10ms total time)
//
// DBUS processing takes 6ms if not tokiod
pub async fn start_daemon() -> Result<(), Box<dyn Error>> {
let laptop = match_laptop();
let mut config = Config::default().load(laptop.supported_modes());
let mut led_control = CtrlKbdBacklight::new(laptop.usb_product(), laptop.supported_modes().to_owned())
.map_or_else(
|err| {
error!("{}", err);
None
},
Some,
);
let mut charge_control = CtrlCharge::new().map_or_else(
|err| {
error!("{}", err);
None
},
Some,
);
let mut fan_control = CtrlFanAndCPU::new().map_or_else(
|err| {
error!("{}", err);
None
},
Some,
);
// Reload settings
if let Some(ctrl) = fan_control.as_mut() {
ctrl.reload_from_config(&mut config)
.await
.unwrap_or_else(|err| warn!("Fan mode: {}", err));
}
if let Some(ctrl) = charge_control.as_mut() {
ctrl.reload_from_config(&mut config)
.await
.unwrap_or_else(|err| warn!("Battery charge limit: {}", err));
}
if let Some(ctrl) = led_control.as_mut() {
ctrl.reload_from_config(&mut config)
.await
.unwrap_or_else(|err| warn!("Reload settings: {}", err));
}
let (resource, connection) = connection::new_system_sync()?;
tokio::spawn(async {
let err = resource.await;
panic!("Lost connection to D-Bus: {}", err);
});
connection
.request_name(DBUS_NAME, false, true, true)
.await?;
let config = Arc::new(Mutex::new(config));
let (
tree,
aura_command_recv,
animatrix_recv,
fan_mode_recv,
charge_limit_recv,
led_changed_signal,
fanmode_signal,
charge_limit_signal,
) = dbus_create_tree(config.clone());
// We add the tree to the connection so that incoming method calls will be handled.
tree.start_receive_send(&*connection);
// Send boot signals
send_boot_signals(
connection.clone(),
config.clone(),
fanmode_signal.clone(),
charge_limit_signal.clone(),
led_changed_signal.clone(),
)
.await?;
// For helping with processing signals
start_signal_task(
connection.clone(),
config.clone(),
fanmode_signal,
charge_limit_signal,
);
// Begin all tasks
let mut handles = Vec::new();
if let Ok(ctrl) = CtrlAnimeDisplay::new() {
handles.push(ctrl.spawn_task(config.clone(), animatrix_recv, None, None));
}
if let Some(ctrl) = fan_control.take() {
handles.push(ctrl.spawn_task(config.clone(), fan_mode_recv, None, None));
}
if let Some(ctrl) = charge_control.take() {
handles.push(ctrl.spawn_task(config.clone(), charge_limit_recv, None, None));
}
if let Some(ctrl) = led_control.take() {
handles.push(ctrl.spawn_task(
config.clone(),
aura_command_recv,
Some(connection.clone()),
Some(led_changed_signal),
));
}
for handle in handles {
handle.await?;
}
Ok(())
}
// TODO: Move these in to the controllers tasks
fn start_signal_task(
connection: Arc<SyncConnection>,
config: Arc<Mutex<Config>>,
fanmode_signal: Arc<Signal<()>>,
charge_limit_signal: Arc<Signal<()>>,
) {
tokio::spawn(async move {
// Some small things we need to track, without passing all sorts of stuff around
let mut last_fan_mode = config.lock().await.fan_mode;
let mut last_charge_limit = config.lock().await.bat_charge_limit;
loop {
// Use tokio sleep to not hold up other threads
tokio::time::delay_for(std::time::Duration::from_millis(500)).await;
let config = config.lock().await;
if config.fan_mode != last_fan_mode {
last_fan_mode = config.fan_mode;
connection
.send(
fanmode_signal
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(last_fan_mode),
)
.unwrap_or_else(|_| 0);
}
if config.bat_charge_limit != last_charge_limit {
last_charge_limit = config.bat_charge_limit;
connection
.send(
charge_limit_signal
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(last_charge_limit),
)
.unwrap_or_else(|_| 0);
}
}
});
}
async fn send_boot_signals(
connection: Arc<SyncConnection>,
config: Arc<Mutex<Config>>,
fanmode_signal: Arc<Signal<()>>,
charge_limit_signal: Arc<Signal<()>>,
led_changed_signal: Arc<Signal<()>>,
) -> Result<(), Box<dyn Error>> {
let config = config.lock().await;
if let Some(data) = config.get_led_mode_data(config.current_mode) {
connection
.send(
led_changed_signal
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(serde_json::to_string(data)?),
)
.unwrap_or_else(|_| 0);
}
connection
.send(
fanmode_signal
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(config.fan_mode),
)
.unwrap_or_else(|_| 0);
connection
.send(
charge_limit_signal
.msg(&DBUS_PATH.into(), &DBUS_IFACE.into())
.append1(config.bat_charge_limit),
)
.unwrap_or_else(|_| 0);
Ok(())
}

235
asus-nb-ctrl/src/dbus.rs Normal file
View File

@@ -0,0 +1,235 @@
use crate::config::Config;
use dbus::tree::{Factory, MTSync, Method, MethodErr, Signal, Tree};
use log::warn;
use asus_nb::{aura_modes::AuraModes, DBUS_IFACE, DBUS_PATH};
use std::sync::Arc;
use tokio::sync::{
mpsc::{channel, Receiver, Sender},
Mutex,
};
fn set_keyboard_backlight(sender: Mutex<Sender<AuraModes>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("SetKeyBacklight", (), {
move |m| {
let json: &str = m.msg.read1()?;
if let Ok(mut lock) = sender.try_lock() {
if let Ok(data) = serde_json::from_str(json) {
lock.try_send(data).unwrap_or_else(|err| {
warn!("SetKeyBacklight over mpsc failed: {}", err)
});
} else {
warn!("SetKeyBacklight could not deserialise");
}
Ok(vec![])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.inarg::<&str, _>("json")
.annotate("org.freedesktop.DBus.Method.NoReply", "true")
}
fn get_keyboard_backlight(config: Arc<Mutex<Config>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
.method("GetKeyBacklight", (), {
move |m| {
if let Ok(lock) = config.try_lock() {
for mode in &lock.builtin_modes {
if lock.current_mode == <u8>::from(mode) {
let mode = serde_json::to_string(&mode).unwrap();
let mret = m.msg.method_return().append1(mode);
return Ok(vec![mret]);
}
}
Err(MethodErr::failed(
"Keyboard LED mode set to an invalid mode",
))
} else {
Err(MethodErr::failed("Could not lock config for access"))
}
}
})
.outarg::<&str, _>("json")
}
fn get_keyboard_backlight_modes(config: Arc<Mutex<Config>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
.method("GetKeyBacklightModes", (), {
move |m| {
if let Ok(lock) = config.try_lock() {
let mode = serde_json::to_string(&lock.builtin_modes).unwrap();
let mret = m.msg.method_return().append1(mode);
Ok(vec![mret])
} else {
Err(MethodErr::failed("Could not lock config for access"))
}
}
})
.outarg::<&str, _>("json")
}
fn set_animatrix(
sender: Mutex<Sender<Vec<Vec<u8>>>>, // need mutex only to get interior mutability in MTSync
) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("AnimatrixWrite", (), {
move |m| {
let mut iter = m.msg.iter_init();
let byte_array: Vec<Vec<u8>> = vec![iter.read()?, iter.read()?];
if let Ok(mut lock) = sender.try_lock() {
// Ignore errors if the channel is already full
lock.try_send(byte_array).unwrap_or_else(|_err| {});
Ok(vec![])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.inarg::<Vec<u8>, _>("bytearray1")
.inarg::<Vec<u8>, _>("bytearray2")
.annotate("org.freedesktop.DBus.Method.NoReply", "true")
}
fn set_fan_mode(sender: Mutex<Sender<u8>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("SetFanMode", (), {
move |m| {
if let Ok(mut lock) = sender.try_lock() {
let mut iter = m.msg.iter_init();
let byte: u8 = iter.read()?;
lock.try_send(byte).unwrap_or_else(|_err| {});
Ok(vec![])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.inarg::<u8, _>("mode")
.annotate("org.freedesktop.DBus.Method.NoReply", "true")
}
fn get_fan_mode(config: Arc<Mutex<Config>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
.method("GetFanMode", (), {
move |m| {
if let Ok(lock) = config.try_lock() {
let mret = m.msg.method_return().append1(lock.fan_mode);
Ok(vec![mret])
} else {
Err(MethodErr::failed("Could not lock config for access"))
}
}
})
.outarg::<u8, _>("mode")
}
fn get_charge_limit(config: Arc<Mutex<Config>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
.method("GetChargeLimit", (), {
move |m| {
if let Ok(lock) = config.try_lock() {
let mret = m.msg.method_return().append1(lock.bat_charge_limit);
Ok(vec![mret])
} else {
Err(MethodErr::failed("Could not lock config for access"))
}
}
})
.outarg::<u8, _>("limit")
}
fn set_charge_limit(sender: Mutex<Sender<u8>>) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("SetChargeLimit", (), {
move |m| {
if let Ok(mut lock) = sender.try_lock() {
let mut iter = m.msg.iter_init();
let byte: u8 = iter.read()?;
lock.try_send(byte).unwrap_or_else(|_err| {});
Ok(vec![])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.inarg::<u8, _>("limit")
.annotate("org.freedesktop.DBus.Method.NoReply", "true")
}
#[allow(clippy::type_complexity)]
pub fn dbus_create_tree(
config: Arc<Mutex<Config>>,
) -> (
Tree<MTSync, ()>,
Receiver<AuraModes>,
Receiver<Vec<Vec<u8>>>,
Receiver<u8>,
Receiver<u8>,
Arc<Signal<()>>,
Arc<Signal<()>>,
Arc<Signal<()>>,
) {
let (aura_command_send, aura_command_recv) = channel::<AuraModes>(1);
let (animatrix_send, animatrix_recv) = channel::<Vec<Vec<u8>>>(1);
let (fan_mode_send, fan_mode_recv) = channel::<u8>(1);
let (charge_send, charge_recv) = channel::<u8>(1);
let factory = Factory::new_sync::<()>();
let key_backlight_changed = Arc::new(
factory
.signal("KeyBacklightChanged", ())
.sarg::<&str, _>("json"),
);
let chrg_limit_changed = Arc::new(
factory
.signal("ChargeLimitChanged", ())
.sarg::<u8, _>("limit"),
);
let fanmode_changed = Arc::new(factory.signal("FanModeChanged", ()).sarg::<u8, _>("mode"));
let tree = factory
.tree(())
.add(
factory.object_path(DBUS_PATH, ()).introspectable().add(
factory
.interface(DBUS_IFACE, ())
.add_m(set_keyboard_backlight(Mutex::new(aura_command_send)))
.add_m(set_animatrix(Mutex::new(animatrix_send)))
.add_m(set_fan_mode(Mutex::new(fan_mode_send)))
.add_m(set_charge_limit(Mutex::new(charge_send)))
.add_m(get_fan_mode(config.clone()))
.add_m(get_charge_limit(config.clone()))
.add_m(get_keyboard_backlight(config.clone()))
.add_m(get_keyboard_backlight_modes(config))
.add_s(key_backlight_changed.clone())
.add_s(fanmode_changed.clone())
.add_s(chrg_limit_changed.clone()),
),
)
.add(factory.object_path("/", ()).introspectable());
(
tree,
aura_command_recv,
animatrix_recv,
fan_mode_recv,
charge_recv,
key_backlight_changed,
fanmode_changed,
chrg_limit_changed,
)
}

19
asus-nb-ctrl/src/error.rs Normal file
View File

@@ -0,0 +1,19 @@
use std::fmt;
#[derive(Debug)]
pub enum RogError {
ParseFanLevel,
NotSupported,
}
impl std::error::Error for RogError {}
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::ParseFanLevel => write!(f, "Parse error"),
RogError::NotSupported => write!(f, "Not supported"),
}
}
}

122
asus-nb-ctrl/src/laptops.rs Normal file
View File

@@ -0,0 +1,122 @@
use log::{info, warn};
use asus_nb::aura_modes::{
AuraModes, BREATHING, COMET, FLASH, HIGHLIGHT, LASER, MULTISTATIC, PULSE, RAIN, RAINBOW, RGB,
RIPPLE, SINGLE, STAR, STROBE,
};
static HELP_ADDRESS: &str = "https://gitlab.com/asus-linux/asus-nb-ctrl";
pub fn match_laptop() -> LaptopBase {
for device in rusb::devices().unwrap().iter() {
let device_desc = device.device_descriptor().unwrap();
if device_desc.vendor_id() == 0x0b05 {
match device_desc.product_id() {
0x1866 => return select_1866_device("1866".to_owned()),
0x1869 => return select_1866_device("1869".to_owned()),
0x1854 => {
info!("Found GL753 or similar");
return LaptopBase {
usb_product: "1854".to_string(),
supported_modes: vec![SINGLE, BREATHING, STROBE],
};
}
_ => {}
}
}
}
panic!("could not match laptop");
}
fn select_1866_device(prod: String) -> LaptopBase {
let dmi = sysfs_class::DmiId::default();
let board_name = dmi.board_name().expect("Could not get board_name");
let prod_name = dmi.product_name().expect("Could not get board_name");
info!("Product name: {}", prod_name.trim());
info!("Board name: {}", board_name.trim());
let mut laptop = LaptopBase {
usb_product: prod,
supported_modes: vec![],
};
// AniMe, no RGB
if board_name.starts_with("GA401")
|| board_name.starts_with("GA502")
|| board_name.starts_with("GU502")
{
info!("No RGB control available");
// RGB, per-key settings, no zones
} else if board_name.starts_with("GX502")
|| board_name.starts_with("GX701")
|| board_name.starts_with("G531")
|| board_name.starts_with("GL531")
|| board_name.starts_with("G532")
{
laptop.supported_modes = vec![
SINGLE, BREATHING, STROBE, RAINBOW, STAR, RAIN, HIGHLIGHT, LASER, RIPPLE, PULSE, COMET,
FLASH, RGB,
];
} else if board_name.starts_with("G531") || board_name.starts_with("G731") {
laptop.supported_modes = vec![
SINGLE,
BREATHING,
STROBE,
RAINBOW,
STAR,
RAIN,
HIGHLIGHT,
LASER,
RIPPLE,
PULSE,
COMET,
FLASH,
MULTISTATIC,
RGB,
];
// RGB, limited effects, no zones
} else if board_name.starts_with("G512LI") || board_name.starts_with("G712LI") {
laptop.supported_modes = vec![SINGLE, BREATHING, STROBE, RAINBOW, PULSE];
// RGB, limited effects, 4-zone RGB
} else if board_name.starts_with("GM501")
|| board_name.starts_with("GX531")
|| board_name.starts_with("G512")
|| board_name.starts_with("G712")
{
laptop.supported_modes = vec![SINGLE, BREATHING, STROBE, RAINBOW, PULSE, MULTISTATIC];
} else {
warn!(
"Unsupported laptop, please request support at {}",
HELP_ADDRESS
);
warn!("Continuing with minimal support")
}
if !laptop.supported_modes.is_empty() {
info!("Supported Keyboard LED modes are:");
for mode in &laptop.supported_modes {
let mode = <&str>::from(&<AuraModes>::from(*mode));
info!("- {}", mode);
}
info!(
"If these modes are incorrect or missing please request support at {}",
HELP_ADDRESS
);
}
laptop
}
pub struct LaptopBase {
usb_product: String,
supported_modes: Vec<u8>,
}
impl LaptopBase {
pub fn usb_product(&self) -> &str {
&self.usb_product
}
pub fn supported_modes(&self) -> &[u8] {
&self.supported_modes
}
}

45
asus-nb-ctrl/src/lib.rs Normal file
View File

@@ -0,0 +1,45 @@
#![deny(unused_must_use)]
/// Configuration loading, saving
pub mod config;
///
pub mod ctrl_anime;
///
pub mod ctrl_charge;
///
pub mod ctrl_fan_cpu;
///
pub mod ctrl_leds;
///
pub mod dbus;
/// Laptop matching to determine capabilities
pub mod laptops;
mod error;
use async_trait::async_trait;
use config::Config;
use std::error::Error;
use std::sync::Arc;
use tokio::sync::{mpsc::Receiver, Mutex};
use tokio::task::JoinHandle;
pub static VERSION: &str = "1.0.0";
use ::dbus::{nonblock::SyncConnection, tree::Signal};
#[async_trait]
pub trait Controller {
type A;
async fn reload_from_config(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>>;
/// Spawn an infinitely running task (usually) which checks a Receiver for input,
/// and may send a signal over dbus
fn spawn_task(
self,
config: Arc<Mutex<Config>>,
recv: Receiver<Self::A>,
connection: Option<Arc<SyncConnection>>,
signal: Option<Arc<Signal<()>>>,
) -> JoinHandle<()>;
}

72
asus-nb-ctrl/src/main.rs Normal file
View File

@@ -0,0 +1,72 @@
use daemon::ctrl_fan_cpu::FanLevel;
use gumdrop::Options;
use log::LevelFilter;
use asus_nb::{
cli_options::{LedBrightness, SetAuraBuiltin},
core_dbus::AuraDbusWriter,
};
use std::io::Write;
#[derive(Options)]
struct CLIStart {
#[options(help = "print help message")]
help: bool,
#[options(help = "show program version number")]
version: bool,
#[options(meta = "VAL", help = "<off, low, med, high>")]
bright: Option<LedBrightness>,
#[options(meta = "FAN", help = "<silent, normal, boost>")]
fan_mode: Option<FanLevel>,
#[options(meta = "CHRG", help = "<20-100>")]
charge_limit: Option<u8>,
#[options(command)]
command: Option<Command>,
}
#[derive(Options)]
enum Command {
#[options(help = "Set the keyboard lighting from built-in modes")]
LedMode(LedModeCommand),
}
#[derive(Options)]
struct LedModeCommand {
#[options(help = "print help message")]
help: bool,
#[options(command, required)]
command: Option<SetAuraBuiltin>,
}
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut logger = env_logger::Builder::new();
logger
.target(env_logger::Target::Stdout)
.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()))
.filter(None, LevelFilter::Info)
.init();
let parsed = CLIStart::parse_args_default_or_exit();
if parsed.version {
println!("Version: {}", daemon::VERSION);
}
let writer = AuraDbusWriter::new()?;
if let Some(Command::LedMode(mode)) = parsed.command {
if let Some(command) = mode.command {
writer.write_builtin_mode(&command.into())?
}
}
if let Some(brightness) = parsed.bright {
writer.write_brightness(brightness.level())?;
}
if let Some(fan_level) = parsed.fan_mode {
writer.write_fan_mode(fan_level.into())?;
}
if let Some(charge_limit) = parsed.charge_limit {
writer.write_charge_limit(charge_limit)?;
}
Ok(())
}