Big refactor out of Aura LED data structs

This commit is contained in:
Luke
2020-05-01 22:07:28 +12:00
parent 38ab4bc182
commit bf6bf2e2f1
23 changed files with 1862 additions and 802 deletions

1016
rog-core/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

45
rog-core/Cargo.toml Normal file
View File

@@ -0,0 +1,45 @@
[package]
name = "rog-daemon"
version = "0.8.0"
license = "MPL-2.0"
readme = "README.md"
authors = ["Luke <luke@ljones.dev>"]
repository = "https://github.com/flukejones/rog-core"
homepage = "https://github.com/flukejones/rog-core"
description = "A daemon app for ASUS GX502 and similar laptops to control missing features"
edition = "2018"
[lib]
name = "daemon"
path = "src/lib.rs"
[[bin]]
name = "rog-core"
path = "src/main.rs"
[dependencies]
rog-aura = { path = "../aura" }
rusb = "^0.5.5"
# cli and logging
gumdrop = "^0.8.0"
log = "^0.4.8"
env_logger = "^0.7.1"
# async
dbus = { version = "^0.8.2", features = ["futures"] }
dbus-tokio = "^0.5.1"
tokio = { version = "0.2.4", features = ["rt-threaded", "macros", "sync"] }
# serialisation
serde = "1.0"
serde_derive = "1.0"
toml = "0.5"
# Device control
# sysfs-class = "^0.1.2" # used for backlight control mostly
# cpu power management
intel-pstate = "^0.2.1"
# virtualisation of HID, mainly for outputting consumer key codes
uhid-virt = "^0.0.4"
#keycode = "0.3"

57
rog-core/src/config.rs Normal file
View File

@@ -0,0 +1,57 @@
use rog_aura::BuiltInModeBytes;
use serde_derive::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
pub static CONFIG_PATH: &str = "/etc/rogcore.conf";
#[derive(Default, Deserialize, Serialize)]
pub struct Config {
pub fan_mode: u8,
pub brightness: u8,
pub current_mode: [u8; 4],
pub builtin_modes: BuiltInModeBytes,
}
impl Config {
pub fn read(mut self) -> 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.current_mode[0] = 0x5d;
c.current_mode[1] = 0xb3;
let toml = toml::to_string(&c).unwrap();
file.write_all(toml.as_bytes())
.expect("Writing default config failed");
self = c;
} else {
self = toml::from_str(&buf).unwrap();
}
}
self
}
pub fn write(&self) {
let mut file = File::create(CONFIG_PATH).expect("Couldn't overwrite config");
let toml = toml::to_string(self).expect("Parse config to JSON failed");
file.write_all(toml.as_bytes())
.expect("Saving config failed");
}
pub fn set_field_from(&mut self, bytes: &[u8]) {
if bytes[0] == 0x5a && bytes[1] == 0xba {
self.brightness = bytes[4];
} else if bytes[0] == 0x5d && bytes[1] == 0xb3 {
self.current_mode.copy_from_slice(&bytes[0..4]);
self.builtin_modes.set_field_from(bytes);
}
}
}

552
rog-core/src/core.rs Normal file
View File

@@ -0,0 +1,552 @@
// Return show-stopping errors, otherwise map error to a log level
use crate::{config::Config, virt_device::VirtKeys};
use gumdrop::Options;
use log::{debug, error, info, warn};
use rog_aura::{aura_brightness_bytes, error::AuraError, BuiltInModeByte};
use rusb::DeviceHandle;
use std::error::Error;
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::marker::{PhantomData, PhantomPinned};
use std::path::Path;
use std::process::Command;
use std::ptr::NonNull;
use std::str::FromStr;
use std::time::Duration;
static LED_INIT1: [u8; 2] = [0x5d, 0xb9];
static LED_INIT2: &str = "]ASUS Tech.Inc."; // ] == 0x5d
static LED_INIT3: [u8; 6] = [0x5d, 0x05, 0x20, 0x31, 0, 0x08];
static LED_INIT4: &str = "^ASUS Tech.Inc."; // ^ == 0x5e
static LED_INIT5: [u8; 6] = [0x5e, 0x05, 0x20, 0x31, 0, 0x08];
// 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];
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";
/// ROG device controller
///
/// For the GX502GW the LED setup sequence looks like:
///
/// -` LED_INIT1`
/// - `LED_INIT3`
/// - `LED_INIT4`
/// - `LED_INIT2`
/// - `LED_INIT4`
pub(crate) struct RogCore {
handle: DeviceHandle<rusb::GlobalContext>,
virt_keys: VirtKeys,
_pin: PhantomPinned,
}
impl RogCore {
pub(crate) fn new(
vendor: u16,
product: u16,
led_endpoint: u8,
) -> Result<RogCore, Box<dyn Error>> {
let mut dev_handle = RogCore::get_device(vendor, product)?;
dev_handle.set_active_configuration(0).unwrap_or(());
let dev_config = dev_handle.device().config_descriptor(0).unwrap();
// Interface with outputs
let mut interface = 0;
for iface in dev_config.interfaces() {
for desc in iface.descriptors() {
for endpoint in desc.endpoint_descriptors() {
if endpoint.address() == led_endpoint {
info!("INTERVAL: {:?}", endpoint.interval());
info!("MAX_PKT_SIZE: {:?}", endpoint.max_packet_size());
info!("SYNC: {:?}", endpoint.sync_type());
info!("TRANSFER_TYPE: {:?}", endpoint.transfer_type());
info!("ENDPOINT: {:X?}", endpoint.address());
interface = desc.interface_number();
break;
}
}
}
}
dev_handle.set_auto_detach_kernel_driver(true).unwrap();
dev_handle.claim_interface(interface)?;
Ok(RogCore {
handle: dev_handle,
virt_keys: VirtKeys::new(),
_pin: PhantomPinned,
})
}
pub(crate) async fn reload(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>> {
// let mode_curr = self.config.current_mode[3];
// let mode = self
// .config
// .builtin_modes
// .get_field_from(BuiltInModeByte::from(mode_curr).into())
// .unwrap()
// .to_owned();
// self.aura_write_messages(&[&mode])?;
let path = if Path::new(FAN_TYPE_1_PATH).exists() {
FAN_TYPE_1_PATH
} else if Path::new(FAN_TYPE_2_PATH).exists() {
FAN_TYPE_2_PATH
} else {
return Ok(());
};
let mut file = OpenOptions::new().write(true).open(path)?;
file.write_all(format!("{:?}\n", config.fan_mode).as_bytes())?;
self.set_pstate_for_fan_mode(FanLevel::from(config.fan_mode))?;
info!("Reloaded last saved settings");
Ok(())
}
pub(crate) fn virt_keys(&mut self) -> &mut VirtKeys {
&mut self.virt_keys
}
fn get_device(
vendor: u16,
product: u16,
) -> Result<DeviceHandle<rusb::GlobalContext>, rusb::Error> {
for device in rusb::devices().unwrap().iter() {
let device_desc = device.device_descriptor().unwrap();
if device_desc.vendor_id() == vendor && device_desc.product_id() == product {
return device.open();
}
}
Err(rusb::Error::NoDevice)
}
pub(crate) fn fan_mode_step(&mut self, config: &mut Config) -> Result<(), Box<dyn Error>> {
let path = if Path::new(FAN_TYPE_1_PATH).exists() {
FAN_TYPE_1_PATH
} else if Path::new(FAN_TYPE_2_PATH).exists() {
FAN_TYPE_2_PATH
} else {
return Ok(());
};
let mut fan_ctrl = OpenOptions::new().read(true).write(true).open(path)?;
let mut buf = String::new();
if let Ok(_) = fan_ctrl.read_to_string(&mut buf) {
let mut n = u8::from_str_radix(&buf.trim_end(), 10)?;
info!("Current fan mode: {:#?}", FanLevel::from(n));
// wrap around the step number
if n < 2 {
n += 1;
} else {
n = 0;
}
info!("Fan mode stepped to: {:#?}", FanLevel::from(n));
fan_ctrl.write_all(format!("{:?}\n", n).as_bytes())?;
self.set_pstate_for_fan_mode(FanLevel::from(n))?;
config.fan_mode = n;
config.write();
}
Ok(())
}
fn set_pstate_for_fan_mode(&self, mode: FanLevel) -> Result<(), Box<dyn Error>> {
// Set CPU pstate
if let Ok(pstate) = intel_pstate::PState::new() {
match mode {
FanLevel::Normal => {
pstate.set_min_perf_pct(0)?;
pstate.set_max_perf_pct(100)?;
pstate.set_no_turbo(false)?;
info!("CPU pstate: normal");
}
FanLevel::Boost => {
pstate.set_min_perf_pct(50)?;
pstate.set_max_perf_pct(100)?;
pstate.set_no_turbo(false)?;
info!("CPU pstate: boost");
}
FanLevel::Silent => {
pstate.set_min_perf_pct(0)?;
pstate.set_max_perf_pct(70)?;
pstate.set_no_turbo(true)?;
info!("CPU pstate: silent, no-turbo");
}
}
}
Ok(())
}
/// A direct call to systemd to suspend the PC.
///
/// This avoids desktop environments being required to handle it
/// (which means it works while in a TTY also)
pub(crate) fn suspend_with_systemd(&self) {
std::process::Command::new("systemctl")
.arg("suspend")
.spawn()
.map_or_else(|err| warn!("Failed to suspend: {}", err), |_| {});
}
/// A direct call to rfkill to suspend wireless devices.
///
/// This avoids desktop environments being required to handle it (which
/// means it works while in a TTY also)
pub(crate) fn toggle_airplane_mode(&self) {
match Command::new("rfkill").arg("list").output() {
Ok(output) => {
if output.status.success() {
if let Ok(out) = String::from_utf8(output.stdout) {
if out.contains(": yes") {
Command::new("rfkill")
.arg("unblock")
.arg("all")
.spawn()
.map_or_else(
|err| warn!("Could not unblock rf devices: {}", err),
|_| {},
);
} else {
Command::new("rfkill")
.arg("block")
.arg("all")
.spawn()
.map_or_else(
|err| warn!("Could not block rf devices: {}", err),
|_| {},
);
}
}
} else {
warn!("Could not list rf devices");
}
}
Err(err) => {
warn!("Could not list rf devices: {}", err);
}
}
}
pub(crate) fn get_raw_device_handle(&mut self) -> NonNull<DeviceHandle<rusb::GlobalContext>> {
// Breaking every damn lifetime guarantee rust gives us
unsafe {
NonNull::new_unchecked(&mut self.handle as *mut DeviceHandle<rusb::GlobalContext>)
}
}
}
/// Lifetime is tied to `DeviceHandle` from `RogCore`
pub(crate) struct KeyboardReader<'d, C: 'd>
where
C: rusb::UsbContext,
{
handle: NonNull<DeviceHandle<C>>,
endpoint: u8,
filter: Vec<u8>,
_phantom: PhantomData<&'d DeviceHandle<C>>,
}
/// UNSAFE
unsafe impl<'d, C> Send for KeyboardReader<'d, C> where C: rusb::UsbContext {}
unsafe impl<'d, C> Sync for KeyboardReader<'d, C> where C: rusb::UsbContext {}
impl<'d, C> KeyboardReader<'d, C>
where
C: rusb::UsbContext,
{
pub fn new(device_handle: NonNull<DeviceHandle<C>>, key_endpoint: u8, filter: Vec<u8>) -> Self {
KeyboardReader {
handle: device_handle,
endpoint: key_endpoint,
filter,
_phantom: PhantomData,
}
}
/// Write the bytes read from the device interrupt to the buffer arg, and returns the
/// count of bytes written
///
/// `report_filter_bytes` is used to filter the data read from the interupt so
/// only the relevant byte array is returned.
pub(crate) async fn poll_keyboard(&self) -> Option<[u8; 32]> {
let mut buf = [0u8; 32];
match unsafe { self.handle.as_ref() }.read_interrupt(
self.endpoint,
&mut buf,
Duration::from_millis(200),
) {
Ok(_) => {
if self.filter.contains(&buf[0])
&& (buf[1] != 0 || buf[2] != 0 || buf[3] != 0 || buf[4] != 0)
{
return Some(buf);
}
}
Err(err) => match err {
rusb::Error::Timeout => {}
_ => error!("Failed to read keyboard interrupt: {:?}", err),
},
}
None
}
}
/// UNSAFE: Must live as long as RogCore
///
/// Because we're holding a pointer to something that *may* go out of scope while the
/// pointer is held. We're relying on access to struct to be behind a Mutex, and for behaviour
/// that may cause invalididated pointer to cause the program to panic rather than continue.
pub(crate) struct LedWriter<'d, C: 'd>
where
C: rusb::UsbContext,
{
handle: NonNull<DeviceHandle<C>>,
led_endpoint: u8,
initialised: bool,
_phantom: PhantomData<&'d DeviceHandle<C>>,
}
/// UNSAFE
unsafe impl<'d, C> Send for LedWriter<'d, C> where C: rusb::UsbContext {}
unsafe impl<'d, C> Sync for LedWriter<'d, C> where C: rusb::UsbContext {}
impl<'d, C> LedWriter<'d, C>
where
C: rusb::UsbContext,
{
pub fn new(device_handle: NonNull<DeviceHandle<C>>, led_endpoint: u8) -> Self {
LedWriter {
handle: device_handle,
led_endpoint,
initialised: false,
_phantom: PhantomData,
}
}
async fn aura_write(&mut self, message: &[u8]) -> Result<(), AuraError> {
match unsafe { self.handle.as_ref() }.write_interrupt(
self.led_endpoint,
message,
Duration::from_millis(1),
) {
Ok(_) => {}
Err(err) => match err {
rusb::Error::Timeout => {}
_ => error!("Failed to read keyboard interrupt: {:?}", err),
},
}
Ok(())
}
async fn aura_write_messages(&mut self, messages: &[&[u8]]) -> Result<(), AuraError> {
if !self.initialised {
self.aura_write(&LED_INIT1).await?;
self.aura_write(LED_INIT2.as_bytes()).await?;
self.aura_write(&LED_INIT3).await?;
self.aura_write(LED_INIT4.as_bytes()).await?;
self.aura_write(&LED_INIT5).await?;
self.initialised = true;
}
for message in messages {
self.aura_write(*message).await?;
self.aura_write(&LED_SET).await?;
}
// Changes won't persist unless apply is set
self.aura_write(&LED_APPLY).await?;
Ok(())
}
/// Write an effect block
///
/// `aura_effect_init` must be called any effect routine, and called only once.
pub async fn async_write_effect(
&self,
endpoint: u8,
effect: Vec<Vec<u8>>,
) -> Result<(), AuraError> {
for row in effect.iter() {
match unsafe { self.handle.as_ref() }.write_interrupt(
endpoint,
row,
Duration::from_millis(1),
) {
Ok(_) => {}
Err(err) => match err {
rusb::Error::Timeout => {}
_ => error!("Failed to write LED interrupt: {:?}", err),
},
}
}
Ok(())
}
pub(crate) async fn aura_set_and_save(
&mut self,
supported_modes: &[BuiltInModeByte],
bytes: &[u8],
config: &mut Config,
) -> Result<(), AuraError> {
let mode = BuiltInModeByte::from(bytes[3]);
if bytes[1] == 0xbc {
self.aura_write(bytes).await?;
return Ok(());
} else if supported_modes.contains(&mode) || bytes[1] == 0xba {
let messages = [bytes];
self.aura_write_messages(&messages).await?;
config.set_field_from(bytes);
config.write();
return Ok(());
}
warn!("{:?} not supported", mode);
Err(AuraError::NotSupported)
}
pub(crate) async fn aura_bright_inc(
&mut self,
supported_modes: &[BuiltInModeByte],
max_bright: u8,
config: &mut Config,
) -> Result<(), AuraError> {
let mut bright = config.brightness;
if bright < max_bright {
bright += 1;
config.brightness = bright;
let bytes = aura_brightness_bytes(bright);
self.aura_set_and_save(supported_modes, &bytes, config)
.await?;
info!("Increased LED brightness to {:#?}", bright);
}
Ok(())
}
pub(crate) async fn aura_bright_dec(
&mut self,
supported_modes: &[BuiltInModeByte],
min_bright: u8,
config: &mut Config,
) -> Result<(), AuraError> {
let mut bright = config.brightness;
if bright > min_bright {
bright -= 1;
config.brightness = bright;
let bytes = aura_brightness_bytes(bright);
self.aura_set_and_save(supported_modes, &bytes, config)
.await?;
info!("Decreased LED brightness to {:#?}", bright);
}
Ok(())
}
/// Select next Aura effect
///
/// If the current effect is the last one then the effect selected wraps around to the first.
pub(crate) async fn aura_mode_next(
&mut self,
supported_modes: &[BuiltInModeByte],
config: &mut Config,
) -> Result<(), AuraError> {
// TODO: different path for multi-zone (byte 2 controlled, non-zero)
let mode_curr = config.current_mode[3];
let idx = supported_modes.binary_search(&mode_curr.into()).unwrap();
let idx_next = if idx < supported_modes.len() - 1 {
idx + 1
} else {
0
};
let mode_next = config
.builtin_modes
.get_field_from(supported_modes[idx_next].into())
.unwrap()
.to_owned();
self.aura_set_and_save(supported_modes, &mode_next, config)
.await?;
info!("Switched LED mode to {:#?}", supported_modes[idx_next]);
Ok(())
}
/// Select previous Aura effect
///
/// If the current effect is the first one then the effect selected wraps around to the last.
pub(crate) async fn aura_mode_prev(
&mut self,
supported_modes: &[BuiltInModeByte],
config: &mut Config,
) -> Result<(), AuraError> {
// TODO: different path for multi-zone (byte 2 controlled, non-zero)
let mode_curr = config.current_mode[3];
let idx = supported_modes.binary_search(&mode_curr.into()).unwrap();
let idx_next = if idx > 0 {
idx - 1
} else {
supported_modes.len() - 1
};
let mode_next = config
.builtin_modes
.get_field_from(supported_modes[idx_next].into())
.unwrap()
.to_owned();
self.aura_set_and_save(supported_modes, &mode_next, config)
.await?;
info!("Switched LED mode to {:#?}", supported_modes[idx_next]);
Ok(())
}
}
#[derive(Debug, Options)]
pub struct LedBrightness {
level: u8,
}
impl LedBrightness {
pub fn level(&self) -> u8 {
self.level
}
}
impl FromStr for LedBrightness {
type Err = AuraError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_str() {
"off" => Ok(LedBrightness { level: 0x00 }),
"low" => Ok(LedBrightness { level: 0x01 }),
"med" => Ok(LedBrightness { level: 0x02 }),
"high" => Ok(LedBrightness { level: 0x03 }),
_ => {
println!("Missing required argument, must be one of:\noff,low,med,high\n");
Err(AuraError::ParseBrightness)
}
}
}
}
#[derive(Debug)]
enum FanLevel {
Normal,
Boost,
Silent,
}
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,
}
}
}

227
rog-core/src/daemon.rs Normal file
View File

@@ -0,0 +1,227 @@
use crate::{config::Config, core::*, laptops::match_laptop};
use dbus::{
nonblock::Process,
tree::{Factory, MTSync, Method, MethodErr, Tree},
};
use dbus_tokio::connection;
use log::{error, info, warn};
use rog_aura::{DBUS_IFACE, DBUS_PATH};
use std::error::Error;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
type LedMsgType = Arc<Mutex<Option<Vec<u8>>>>;
type EffectType = Arc<Mutex<Option<Vec<Vec<u8>>>>>;
// 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().read();
let mut rogcore = RogCore::new(
laptop.usb_vendor(),
laptop.usb_product(),
laptop.led_endpoint(),
)
.map_or_else(
|err| {
error!("{}", err);
panic!("{}", err);
},
|daemon| {
info!("RogCore loaded");
daemon
},
);
// Reload settings
rogcore.reload(&mut config).await?;
// Set up the mutexes
let led_writer = Arc::new(Mutex::new(LedWriter::new(
rogcore.get_raw_device_handle(),
laptop.led_endpoint(),
)));
let config = Arc::new(Mutex::new(config));
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_IFACE, false, true, true)
.await?;
let (tree, input, effect) = dbus_create_tree();
// We add the tree to the connection so that incoming method calls will be handled.
tree.start_receive_send(&*connection);
let supported = Vec::from(laptop.supported_modes());
let led_endpoint = laptop.led_endpoint();
// Keyboard reader goes in separate task because we want a high interrupt timeout
// and don't want that to hold up other tasks, or miss keystrokes
let keyboard_reader = KeyboardReader::new(
rogcore.get_raw_device_handle(),
laptop.key_endpoint(),
laptop.key_filter().to_owned(),
);
let led_writer1 = led_writer.clone();
let config1 = config.clone();
// start the keyboard reader and laptop-action loop
let key_read_handle = tokio::spawn(async move {
loop {
let data = keyboard_reader.poll_keyboard().await;
if let Some(bytes) = data {
laptop
.run(&mut rogcore, &led_writer1, &config1, bytes)
.await
.map_err(|err| warn!("{:?}", err))
.unwrap();
}
}
});
// start the LED writer loop
let led_write_handle = tokio::spawn(async move {
let mut time_mark = Instant::now();
loop {
connection.process_all();
let led_writer = led_writer.clone();
if let Ok(mut lock) = input.try_lock() {
if let Some(bytes) = lock.take() {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_set_and_save(&supported, &bytes, &mut config)
.await
.map_err(|err| warn!("{:?}", err))
.unwrap();
time_mark = Instant::now();
}
}
// Write a colour block
let led_writer = led_writer.clone();
if let Ok(mut lock) = effect.try_lock() {
// Spawn a writer
if let Some(stuff) = lock.take() {
let led_writer = led_writer.lock().await;
led_writer
.async_write_effect(led_endpoint, stuff)
.await
.map_err(|err| warn!("{:?}", err))
.unwrap();
time_mark = Instant::now();
}
}
let now = Instant::now();
// Cool-down steps
// This block is to prevent the loop spooling as fast as possible and saturating the CPU
if now.duration_since(time_mark).as_millis() > 500 {
std::thread::sleep(Duration::from_millis(200));
} else if now.duration_since(time_mark).as_millis() > 100 {
std::thread::sleep(Duration::from_millis(50));
} else {
std::thread::sleep(Duration::from_micros(300));
}
}
});
led_write_handle.await?;
key_read_handle.await?;
Ok(())
}
fn dbus_create_ledmsg_method(msg: LedMsgType) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("ledmessage", (), {
move |m| {
let bytes: Vec<u8> = m.msg.read1()?;
if let Ok(mut lock) = msg.try_lock() {
*lock = Some(bytes.to_vec());
let mret = m
.msg
.method_return()
.append1(&format!("Wrote {:x?}", bytes));
Ok(vec![mret])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.outarg::<&str, _>("reply")
.inarg::<Vec<u8>, _>("bytearray")
}
fn dbus_create_ledeffect_method(effect: EffectType) -> Method<MTSync, ()> {
let factory = Factory::new_sync::<()>();
factory
// method for ledmessage
.method("ledeffect", (), {
move |m| {
if let Ok(mut lock) = effect.try_lock() {
let mut iter = m.msg.iter_init();
let byte_array: Vec<Vec<u8>> = vec![
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
iter.read()?,
];
*lock = Some(byte_array);
let mret = m.msg.method_return().append1(&format!("Got effect part"));
Ok(vec![mret])
} else {
Err(MethodErr::failed("Could not lock daemon for access"))
}
}
})
.outarg::<&str, _>("reply")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
.inarg::<Vec<u8>, _>("bytearray")
}
fn dbus_create_tree() -> (Tree<MTSync, ()>, LedMsgType, EffectType) {
let input: LedMsgType = Arc::new(Mutex::new(None));
let effect: EffectType = Arc::new(Mutex::new(None));
let factory = Factory::new_sync::<()>();
let tree = factory.tree(()).add(
factory.object_path(DBUS_PATH, ()).add(
factory
.interface(DBUS_IFACE, ())
.add_m(dbus_create_ledmsg_method(input.clone()))
.add_m(dbus_create_ledeffect_method(effect.clone())),
),
);
(tree, input, effect)
}

341
rog-core/src/laptops/mod.rs Normal file
View File

@@ -0,0 +1,341 @@
use crate::config::Config;
use crate::core::{LedWriter, RogCore};
use rog_aura::{error::AuraError, BuiltInModeByte};
//use keycode::{KeyMap, KeyMappingId, KeyState, KeyboardState};
use crate::virt_device::ConsumerKeys;
use log::{info, warn};
pub(crate) 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() {
0x1869 | 0x1866 => {
info!("Found GX502 or similar");
return LaptopBase {
usb_vendor: 0x0B05,
usb_product: 0x1866,
report_filter_bytes: vec![0x5a, 0x02],
min_led_bright: 0x00,
max_led_bright: 0x03,
//from `lsusb -vd 0b05:1866`
led_endpoint: 0x04,
//from `lsusb -vd 0b05:1866`
key_endpoint: 0x83,
supported_modes: vec![
BuiltInModeByte::Single,
BuiltInModeByte::Breathing,
BuiltInModeByte::Cycle,
BuiltInModeByte::Rainbow,
BuiltInModeByte::Rain,
BuiltInModeByte::Random,
BuiltInModeByte::Highlight,
BuiltInModeByte::Laser,
BuiltInModeByte::Ripple,
BuiltInModeByte::Pulse,
BuiltInModeByte::ThinZoomy,
BuiltInModeByte::WideZoomy,
],
//backlight: Backlight::new("intel_backlight").unwrap(),
};
}
0x1854 => {
info!("Found GL753 or similar");
return LaptopBase {
usb_vendor: 0x0B05,
usb_product: 0x1854,
report_filter_bytes: vec![0x5a, 0x02],
min_led_bright: 0x00,
max_led_bright: 0x03,
//from `lsusb -vd 0b05:1866`
led_endpoint: 0x04,
//from `lsusb -vd 0b05:1866`
key_endpoint: 0x83,
supported_modes: vec![
BuiltInModeByte::Single,
BuiltInModeByte::Breathing,
BuiltInModeByte::Cycle,
],
// backlight: Backlight::new("intel_backlight").unwrap(),
};
}
_ => {}
}
}
}
panic!("could not match laptop");
}
pub(super) struct LaptopBase {
usb_vendor: u16,
usb_product: u16,
report_filter_bytes: Vec<u8>,
min_led_bright: u8,
max_led_bright: u8,
led_endpoint: u8,
key_endpoint: u8,
supported_modes: Vec<BuiltInModeByte>,
//backlight: Backlight,
}
use tokio::sync::Mutex;
impl LaptopBase {
/// Pass in LedWriter as Mutex so it is only locked when required
pub(super) async fn run<'a, C>(
&self,
rogcore: &mut RogCore,
led_writer: &Mutex<LedWriter<'a, C>>,
config: &Mutex<Config>,
key_buf: [u8; 32],
) -> Result<(), AuraError>
where
C: rusb::UsbContext,
{
match self.usb_product {
0x1869 | 0x1866 => {
self.gx502_runner(rogcore, led_writer, config, key_buf)
.await
}
0x1854 => {
self.gl753_runner(rogcore, led_writer, config, key_buf)
.await
}
_ => panic!("No runner available for this device"),
}
}
pub(super) fn led_endpoint(&self) -> u8 {
self.led_endpoint
}
pub(super) fn key_endpoint(&self) -> u8 {
self.key_endpoint
}
pub(super) fn key_filter(&self) -> &[u8] {
&self.report_filter_bytes
}
pub(super) fn usb_vendor(&self) -> u16 {
self.usb_vendor
}
pub(super) fn usb_product(&self) -> u16 {
self.usb_product
}
pub(super) fn supported_modes(&self) -> &[BuiltInModeByte] {
&self.supported_modes
}
// 0x1866, per-key LEDs, media-keys split from vendor specific
async fn gx502_runner<'a, C>(
&self,
rogcore: &mut RogCore,
led_writer: &Mutex<LedWriter<'a, C>>,
config: &Mutex<Config>,
key_buf: [u8; 32],
) -> Result<(), AuraError>
where
C: rusb::UsbContext,
{
let max_led_bright = self.max_led_bright;
let min_led_bright = self.min_led_bright;
let supported_modes = self.supported_modes.to_owned();
match GX502Keys::from(key_buf[1]) {
GX502Keys::LedBrightUp => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_bright_inc(&supported_modes, max_led_bright, &mut config)
.await?;
}
GX502Keys::LedBrightDown => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_bright_dec(&supported_modes, min_led_bright, &mut config)
.await?;
}
GX502Keys::AuraNext => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_mode_next(&supported_modes, &mut config)
.await?;
}
GX502Keys::AuraPrevious => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_mode_prev(&supported_modes, &mut config)
.await?;
}
GX502Keys::ScreenBrightUp => {
rogcore.virt_keys().press(ConsumerKeys::BacklightInc.into())
} //self.backlight.step_up(),
GX502Keys::ScreenBrightDown => {
rogcore.virt_keys().press(ConsumerKeys::BacklightDec.into())
} //self.backlight.step_down(),
GX502Keys::Sleep => rogcore.suspend_with_systemd(),
GX502Keys::AirplaneMode => rogcore.toggle_airplane_mode(),
GX502Keys::MicToggle => {}
GX502Keys::Fan => {
let mut config = config.lock().await;
rogcore.fan_mode_step(&mut config).unwrap_or_else(|err| {
warn!("Couldn't toggle fan mode: {:?}", err);
});
}
GX502Keys::ScreenToggle => {
rogcore.virt_keys().press(ConsumerKeys::BacklightTog.into());
}
GX502Keys::TouchPadToggle => {
let mut key = [0u8; 32];
key[0] = 0x01;
key[3] = 0x070;
rogcore.virt_keys().press(key);
}
GX502Keys::Rog => {
//rogcore.aura_effect_init()?;
//rogcore.aura_write_effect(&self.per_key_led)?;
let mut key = [0u8; 32];
key[0] = 0x01;
key[3] = 0x68; // XF86Tools? F13
rogcore.virt_keys().press(key);
}
GX502Keys::None => {
if key_buf[0] != 0x5A {
info!("Unmapped key, attempt passthrough: {:X?}", &key_buf[1]);
rogcore.virt_keys().press(key_buf);
}
}
}
Ok(())
}
// GL753VE == 0x1854, 4 zone keyboard
async fn gl753_runner<'a, C>(
&self,
rogcore: &mut RogCore,
led_writer: &Mutex<LedWriter<'a, C>>,
config: &Mutex<Config>,
key_buf: [u8; 32],
) -> Result<(), AuraError>
where
C: rusb::UsbContext,
{
let max_led_bright = self.max_led_bright;
let min_led_bright = self.min_led_bright;
let supported_modes = self.supported_modes.to_owned();
match GL753Keys::from(key_buf[1]) {
GL753Keys::LedBrightUp => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_bright_inc(&supported_modes, max_led_bright, &mut config)
.await?;
}
GL753Keys::LedBrightDown => {
let mut led_writer = led_writer.lock().await;
let mut config = config.lock().await;
led_writer
.aura_bright_dec(&supported_modes, min_led_bright, &mut config)
.await?;
}
GL753Keys::ScreenBrightUp => {
rogcore.virt_keys().press(ConsumerKeys::BacklightInc.into())
}
GL753Keys::ScreenBrightDown => {
rogcore.virt_keys().press(ConsumerKeys::BacklightDec.into())
}
GL753Keys::Sleep => rogcore.suspend_with_systemd(),
GL753Keys::AirplaneMode => rogcore.toggle_airplane_mode(),
GL753Keys::ScreenToggle => {
rogcore.virt_keys().press(ConsumerKeys::BacklightTog.into());
}
GL753Keys::TouchPadToggle => {
let mut key = [0u8; 32];
key[0] = 0x01;
key[3] = 0x070;
rogcore.virt_keys().press(key);
}
GL753Keys::Rog => {
let mut key = [0u8; 32];
key[0] = 0x01;
key[3] = 0x68; // XF86Tools? F13
rogcore.virt_keys().press(key);
}
GL753Keys::None => {
if key_buf[0] != 0x5A {
info!("Unmapped key, attempt passthrough: {:X?}", &key_buf[1]);
rogcore.virt_keys().press(key_buf);
}
}
}
Ok(())
}
}
pub(crate) enum GX502Keys {
Rog = 0x38,
MicToggle = 0x7C,
Fan = 0xAE,
ScreenToggle = 0x35,
ScreenBrightDown = 0x10,
ScreenBrightUp = 0x20,
TouchPadToggle = 0x6b,
Sleep = 0x6C,
AirplaneMode = 0x88,
LedBrightUp = 0xC4,
LedBrightDown = 0xC5,
AuraPrevious = 0xB2,
AuraNext = 0xB3,
None,
}
impl From<u8> for GX502Keys {
fn from(byte: u8) -> Self {
match byte {
0x38 => GX502Keys::Rog,
0x7C => GX502Keys::MicToggle,
0xAE => GX502Keys::Fan,
0x35 => GX502Keys::ScreenToggle,
0x10 => GX502Keys::ScreenBrightDown,
0x20 => GX502Keys::ScreenBrightUp,
0x6b => GX502Keys::TouchPadToggle,
0x6C => GX502Keys::Sleep,
0x88 => GX502Keys::AirplaneMode,
0xC4 => GX502Keys::LedBrightUp,
0xC5 => GX502Keys::LedBrightDown,
0xB2 => GX502Keys::AuraPrevious,
0xB3 => GX502Keys::AuraNext,
_ => GX502Keys::None,
}
}
}
enum GL753Keys {
Rog = 0x38,
ScreenToggle = 0x35,
ScreenBrightDown = 0x10,
ScreenBrightUp = 0x20,
TouchPadToggle = 0x6b,
Sleep = 0x6C,
AirplaneMode = 0x88,
LedBrightUp = 0xC4,
LedBrightDown = 0xC5,
None,
}
impl From<u8> for GL753Keys {
fn from(byte: u8) -> Self {
match byte {
0x38 => GL753Keys::Rog,
0x35 => GL753Keys::ScreenToggle,
0x10 => GL753Keys::ScreenBrightDown,
0x20 => GL753Keys::ScreenBrightUp,
0x6b => GL753Keys::TouchPadToggle,
0x6C => GL753Keys::Sleep,
0x88 => GL753Keys::AirplaneMode,
0xC4 => GL753Keys::LedBrightUp,
0xC5 => GL753Keys::LedBrightDown,
_ => GL753Keys::None,
}
}
}

7
rog-core/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
mod config;
/// The core module which allows writing to LEDs or polling the
/// laptop keyboard attached devices
pub mod core;
pub mod daemon;
pub mod laptops;
mod virt_device;

74
rog-core/src/main.rs Normal file
View File

@@ -0,0 +1,74 @@
use daemon::{core::LedBrightness, daemon::start_daemon};
use env_logger::{Builder, Target};
use gumdrop::Options;
use log::LevelFilter;
use rog_aura::{cli_options::SetAuraBuiltin, AuraDbusWriter, LED_MSG_LEN};
static VERSION: &'static str = "0.8.0";
#[derive(Debug, Options)]
struct CLIStart {
#[options(help = "print help message")]
help: bool,
#[options(help = "show program version number")]
version: bool,
#[options(help = "start daemon")]
daemon: bool,
#[options(meta = "VAL", help = "<off, low, med, high>")]
bright: Option<LedBrightness>,
#[options(command)]
command: Option<Command>,
}
#[derive(Debug, Options)]
enum Command {
#[options(help = "Set the keyboard lighting from built-in modes")]
LedMode(LedModeCommand),
}
#[derive(Debug, 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 builder = Builder::new();
builder.target(Target::Stdout);
builder.format_timestamp(None);
builder.filter(None, LevelFilter::Info).init();
let parsed = CLIStart::parse_args_default_or_exit();
if parsed.daemon {
start_daemon().await?;
}
if parsed.version {
println!("Version: {}", VERSION);
}
let writer = AuraDbusWriter::new()?;
if let Some(Command::LedMode(mode)) = parsed.command {
if let Some(command) = mode.command {
// Check for special modes here, eg, per-key or multi-zone
match command {
SetAuraBuiltin::MultiStatic(_) => {
let byte_arr = <[[u8; LED_MSG_LEN]; 4]>::from(command);
for arr in byte_arr.iter() {
writer.write_bytes(arr)?;
}
}
_ => {
writer.write_builtin_mode(&command)?;
}
}
}
}
if let Some(brightness) = parsed.bright {
writer.write_brightness(brightness.level())?;
}
Ok(())
}

166
rog-core/src/virt_device.rs Normal file
View File

@@ -0,0 +1,166 @@
use uhid_virt::{Bus, CreateParams, UHIDDevice};
/// Create a virtual device to emit key-presses
///
/// This is required in some instances because either the USB device that
/// an interface for a working set of buttons is also captured, or because
/// there is no equivalent "system" action to take for a key function and
/// a key-press is required to emit a particular key code.
///
/// The two devices set up mirror that of the GX502GW and can accept the same
/// original byte arrays to emit.
/// - "Consumer Device" generally has all device type controls like media, backlight, power
/// - "Keyboard Device" is a full featured keyboard including special keys like F13-F24
///
/// # Some example uses:
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x68, 0, 0, 0, 0]); // F13, Config/Control Panel`
///
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x70, 0, 0, 0, 0]); // F21, Touchpad toggle, XF86/Gnome`
///
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x71, 0, 0, 0, 0]); // F22, Touchpad on, XF86/Gnome`
///
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x72, 0, 0, 0, 0]); // F23, Touchpad off, XF86/Gnome`
///
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x82, 0, 0, 0, 0]); // Sleep`
///
/// `rogcore.virt_keys().press([0x01, 0, 0, 0x66, 0, 0, 0, 0]); // Power (menu)`
pub(crate) struct VirtKeys {
device: UHIDDevice<std::fs::File>,
}
impl VirtKeys {
pub(crate) fn new() -> Self {
VirtKeys {
device: UHIDDevice::create(CreateParams {
name: String::from("Virtual ROG buttons"),
phys: String::from(""),
uniq: String::from(""),
bus: Bus::USB,
vendor: 0x0b05,
product: 0x1866,
version: 0,
country: 0,
// This is a device which emits the usage code as a whole, rather than as bits
rd_data: [
// Consumer Device TLC
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x19, 0x00, // Usage Minimum (Unassigned)
0x2A, 0x3C, 0x02, // Usage Maximum (AC Format)
0x15, 0x00, // Logical Minimum (0)
0x26, 0x3C, 0x02, // Logical Maximum (572)
0x75, 0x10, // Report Size (16)
0x95, 0x02, // Report Count (2)
0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State)
0xC0, //
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x05, 0x07, // Usage Page (Kbrd/Keypad)
0x19, 0xE0, // Usage Minimum (0xE0)
0x29, 0xE7, // Usage Maximum (0xE7)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (LEDs)
0x19, 0x01, // Usage Minimum (Num Lock)
0x29, 0x05, // Usage Maximum (Kana)
0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x03, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State)
0x95, 0x1E, // Report Count (30)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x05, 0x07, // Usage Page (Kbrd/Keypad)
0x19, 0x00, // Usage Minimum (0x00)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State)
0xC0, // End Collection
]
.to_vec(),
})
.unwrap(),
}
}
/// A single on/off key press
pub(crate) fn press(&mut self, input: [u8; 32]) {
self.device.write(&input).unwrap();
let mut reset = [0u8; 32];
reset[0] = input[0];
self.device.write(&reset).unwrap();
}
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub(crate) enum ConsumerKeys {
Power = 0x30,
Sleep = 0x32,
Menu = 0x0040,
MediaRecord = 0xB2,
MediaFastFwd = 0xB3,
MediaRewind = 0xB4,
MediaNext = 0xB5,
MediaPrev = 0xB6,
MediaStop = 0xB7,
MediaPlayPause = 0xCD,
MediaPause = 0xB0,
MediaVolMute = 0xE2,
MediaVolUp = 0xE9,
MediaVolDown = 0xEA,
BacklightInc = 0x006F,
BacklightDec = 0x0070,
BacklightTog = 0x072, // USAGE (Backlight toggle? display toggle?)
BacklightMin = 0x73,
BacklightMax = 0x74,
ControlConfig = 0x183,
LaunchWordEditor = 0x184,
LaunchTextEditor = 0x185,
LaunchSpreadSheet = 0x186,
LaunchGraphicsEditor = 0x187,
LaunchPresentationApp = 0x188,
LaunchDatabaseEditor = 0x189,
LaunchEmailApp = 0x18A,
LaunchNewsReader = 0x18B,
LaunchCalendarApp = 0x018e,
LaunchTaskManagementApp = 0x18F,
LaunchWebBrowser = 0x196,
ControlPanel = 0x19F,
VideoOutStep = 0x82,
Documents = 0x1A7,
FileBrowser = 0x1B4,
ImageBrowser = 0x1B6,
AudioBrowser = 0x1B7,
MovieBrowser = 0x1B8,
}
impl From<ConsumerKeys> for [u8; 32] {
fn from(key: ConsumerKeys) -> Self {
let mut bytes = [0u8; 32];
bytes[0] = 0x02; // report ID for consumer
bytes[1] = key as u8;
bytes[2] = (key as u16 >> 8) as u8;
bytes
}
}