From 62ba2376ce60a89fe33d917f2dcd24a61f876dc5 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Tue, 30 Apr 2024 21:01:36 +0200 Subject: [PATCH 1/6] Add a color (2 colors can be now defined for a machine). --- src/machine.rs | 23 +++++++++++++++-------- src/main.rs | 2 ++ src/main_loop.rs | 38 +++++++++++++++++++++++++++++++++----- src/rgb.rs | 2 +- src/settings.rs | 12 ++++++++---- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/machine.rs b/src/machine.rs index 1012c00..1d7bb2e 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -12,7 +12,12 @@ const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; const GIGABYTE_RTX3080TI_VISION_OC_ADDR: u8 = 0x63; pub trait Machine { - fn set_color(&mut self, color: &rgb::RGB); + fn set_color(&mut self, color: &rgb::RGB) { + self.set_color_1(&color); + self.set_color_2(&color); + } + fn set_color_1(&mut self, color: &rgb::RGB); + fn set_color_2(&mut self, color: &rgb::RGB); fn get_gpu_tmp(&self) -> f32; fn get_cpu_tmp(&self) -> f32; } @@ -42,16 +47,15 @@ impl MachineJiji { } impl Machine for MachineJiji { - fn set_color(&mut self, color: &rgb::RGB) { + fn set_color_1(&mut self, color: &rgb::RGB) { for controller in &self.ram { controller.set_color(&color); } - self.b650e_device.set_color(&color); - // if let Err(error) = self.a770.set_color(color.red, color.green, color.blue) { - // error!("Unable to set color: {:?}", error); - // } + self.b650e_device.set_color(&color).unwrap(); } + fn set_color_2(&mut self, color: &rgb::RGB) {} // No color 2. + fn get_gpu_tmp(&self) -> f32 { // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } self.gpus[0].thermal_settings(None).unwrap()[0] @@ -160,13 +164,16 @@ impl MachineLyssMetal { } impl Machine for MachineLyssMetal { - fn set_color(&mut self, color: &rgb::RGB) { + fn set_color_1(&mut self, color: &rgb::RGB) { self.crosshair_device.set_color(&color).unwrap(); self.corsair_lignting_pro.set_color(&color); - self.lian_li_sl_infinity.set_color(&color); // self.set_color_3080ti(&color); // TODO. } + fn set_color_2(&mut self, color: &rgb::RGB) { + self.lian_li_sl_infinity.set_color(&color); + } + fn get_gpu_tmp(&self) -> f32 { self.gpus[0].thermal_settings(None).unwrap()[0] .current_temperature diff --git a/src/main.rs b/src/main.rs index 014c61d..a0443f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,8 @@ mod settings; mod tests; mod timer; +// Important: when starting as a service, the directory where the log and config files +// are put is 'C:\Windows\System32\config\systemprofile\AppData\Roaming\Temp2RGB'. fn main() -> Result<()> { let is_debug = cfg!(debug_assertions); diff --git a/src/main_loop.rs b/src/main_loop.rs index bc2f6ee..a11c36b 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -20,7 +20,21 @@ pub fn main_loop(completed: Arc) { winring0::init(); let sleep = timer::Sleep::new(); - let settings = settings::Settings::read(consts::FILE_CONF).expect("Cannot load settings"); + + let file_conf_path = if cfg!(debug_assertions) { + String::from(consts::FILE_CONF) + } else { + String::from( + dirs::config_dir() + .unwrap() + .join(consts::SERVICE_NAME) + .join(consts::FILE_CONF) + .to_str() + .unwrap(), + ) + }; + + let settings = settings::Settings::read(&file_conf_path).expect("Cannot load settings"); println!("Settings: {settings:?}"); let mut machine: Box = match settings.machine_name { @@ -63,14 +77,28 @@ pub fn main_loop(completed: Arc) { 1f32, ); // Between 0 (cold) and 1 (hot). - let color = - rgb::linear_interpolation(settings.cold_color, settings.hot_color, normalized_temp); + let color_1 = + rgb::linear_interpolation(settings.cold_color_1, settings.hot_color_1, normalized_temp); + + let color_2 = match (settings.cold_color_2, settings.hot_color_2) { + (Some(cold_color), Some(hot_color)) => Some(rgb::linear_interpolation( + cold_color, + hot_color, + normalized_temp, + )), + _ => None, + }; // println!("normalized_temp: {normalized_temp}"); if tick % (consts::FREQ_TEMP_POLLING / consts::FREQ_REFRESHING_RGB) as i64 == 0 { - println!("Update RGB: {color:?}, temp: {mean_temp}"); - machine.set_color(&color); + println!("Update RGB: {color_1:?}/{color_2:?}, temp: {mean_temp}"); + machine.set_color_1(&color_1); + if color_2.is_some() { + machine.set_color_2(&color_2.unwrap()); + } else { + machine.set_color_2(&color_1); + } } let elapsed = time::Instant::now() - time_beginning_loop; diff --git a/src/rgb.rs b/src/rgb.rs index de1161a..a6217c7 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct RGB { pub red: u8, pub green: u8, diff --git a/src/settings.rs b/src/settings.rs index 9939178..511cb8c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -17,8 +17,10 @@ pub enum MachineName { #[derive(Debug, Deserialize, Serialize)] pub struct Settings { pub machine_name: MachineName, - pub cold_color: RGB, - pub hot_color: RGB, + pub cold_color_1: RGB, + pub hot_color_1: RGB, + pub cold_color_2: Option, + pub hot_color_2: Option, // Average temperature between CPU and GPU. pub cold_temperature: f32, pub hot_temperature: f32, @@ -30,16 +32,18 @@ impl Settings { fn default() -> Self { Settings { machine_name: MachineName::Jiji, - cold_color: RGB { + cold_color_1: RGB { red: 0, green: 255, blue: 40, }, - hot_color: RGB { + hot_color_1: RGB { red: 255, green: 0, blue: 0, }, + cold_color_2: None, + hot_color_2: None, cold_temperature: 55., hot_temperature: 75., } -- 2.50.0 From 5e8e7cbc6f56aa1d42b3ae9b6174900fc77fd49f Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Mon, 26 Aug 2024 13:50:02 +0200 Subject: [PATCH 2/6] Update dependencies --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a69afbe..82d89af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ num = "0.4" dirs = "5.0" anyhow = "1.0" -flexi_logger = "0.28" +flexi_logger = "0.29" log-panics = { version = "2", features = ["with-backtrace"] } log = "0.4" @@ -32,13 +32,13 @@ nvapi = "0.1" libc = "0.2" wmi = "0.13" -crc = "3.0" +crc = "3.2" # libloading = "0.8" # netcorehost = "0.15" [dependencies.windows] -version = "0.56" +version = "0.58" features = [ "Win32_Foundation", "Win32_Security", @@ -54,7 +54,7 @@ features = [ ] [build-dependencies] -bindgen = "0.69" +bindgen = "0.70" [profile.release] # strip = "debuginfo" -- 2.50.0 From 1598e623c8de3c6fb3fe81bc8d402fc3d9c7080c Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Mon, 17 Feb 2025 17:56:09 +0100 Subject: [PATCH 3/6] Split machine module in sub modules. Add gigabyte_rgb_fusion module --- Cargo.toml | 11 +- src/gigabyte_rgb_fusion_usb.rs | 206 ++++++++++++++++++++++ src/lian_li_sl_infinity.rs | 6 +- src/machine/jiji.rs | 57 ++++++ src/{machine.rs => machine/lyss_metal.rs} | 91 ++-------- src/machine/lyss_metal2.rs | 114 ++++++++++++ src/machine/mod.rs | 21 +++ src/main.rs | 3 +- src/main_loop.rs | 15 +- src/piix4_i2c.rs | 15 +- src/settings.rs | 1 + src/tests.rs | 21 ++- 12 files changed, 453 insertions(+), 108 deletions(-) create mode 100644 src/gigabyte_rgb_fusion_usb.rs create mode 100644 src/machine/jiji.rs rename src/{machine.rs => machine/lyss_metal.rs} (54%) create mode 100644 src/machine/lyss_metal2.rs create mode 100644 src/machine/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 82d89af..ce95993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,12 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } -ron = "0.8" # Rust object notation, to load configuration files. +# Rust object notation, to load configuration files. +ron = "0.8" num = "0.4" -dirs = "5.0" +dirs = "6.0" anyhow = "1.0" flexi_logger = "0.29" @@ -31,14 +32,14 @@ hidapi = "2.6" nvapi = "0.1" libc = "0.2" -wmi = "0.13" +wmi = "0.15" crc = "3.2" # libloading = "0.8" # netcorehost = "0.15" [dependencies.windows] -version = "0.58" +version = "0.59" features = [ "Win32_Foundation", "Win32_Security", @@ -54,7 +55,7 @@ features = [ ] [build-dependencies] -bindgen = "0.70" +bindgen = "0.71" [profile.release] # strip = "debuginfo" diff --git a/src/gigabyte_rgb_fusion_usb.rs b/src/gigabyte_rgb_fusion_usb.rs new file mode 100644 index 0000000..c9667be --- /dev/null +++ b/src/gigabyte_rgb_fusion_usb.rs @@ -0,0 +1,206 @@ +use std::{str, time::Duration}; + +use crate::rgb::RGB; + +const VID: u16 = 0x048D; // Vendor ID: Gigabyte. +const PID: u16 = 0x5711; // Product ID. + +/* +HidDeviceInfo { vendor_id: 1165, product_id: 22289 } +name: GIGABYTE Device +interface number: 1 +page: 65417 +usage: 204 +*/ + +pub struct Device { + device: hidapi::HidDevice, +} + +impl Device { + pub fn new(api: &hidapi::HidApi) -> anyhow::Result { + let d = api + .device_list() + .find(|d| d.vendor_id() == VID && d.product_id() == PID && d.usage() == 204) + .unwrap() + .open_device(api) + .unwrap(); + + let device = Device { device: d }; + + // Initialization? + let mut buffer = [0u8; 64]; + buffer[0] = 0xCC; + buffer[1] = 0x60; + device.device.send_feature_report(&buffer)?; + + Ok(device) + } + + pub fn test_raw_data(&self) -> anyhow::Result<()> { + loop { + self.send_str( + " +// cc20010000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cc22040000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc23080000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc24100000000000 000000015a006127 b70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc25200000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc26400000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc27800000000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc91000200000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc92000400000000 000000015a00b727 610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc34110100000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + +cc580000392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +// cc583900392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +// cc587200392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +// cc58ab00092761b7 2761b72761b70000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + +// Arctic freezer 3 +cc620000392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +cc623900392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +// cc627200392761b7 2761b72761b72761 b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b72761b70000 +// cc62ab00092761b7 2761b72761b70000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")?; + std::thread::sleep(Duration::from_secs(1)); + self.send_str( + " +// cc20010000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cc22040000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc23080000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc24100000000000 000000015a0035ab 430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc25200000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc26400000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc27800000000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc91000200000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc92000400000000 000000015a0043ab 350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc28ff0700000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +// cc34110100000000 0000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + +cc58000039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab45430000 +// cc58390039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab45430000 +// cc58720039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab45430000 +// cc58ab0009ab4543 ab4543ab45430000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + +// Arctic freezer 3 +cc62000039 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 ab4543 0000 +cc62390039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab45430000 +// cc62720039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab45430000 +// cc62ab0009ab4543 ab4543ab45430000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +")?; + std::thread::sleep(Duration::from_secs(1)); + } + } + + fn line_to_bytes(line: &str) -> Vec { + let line = line.replace(" ", ""); + let mut buffer = vec![0u8; line.len() / 2]; + for i in 0..buffer.len() { + buffer[i] = u8::from_str_radix(&line[2 * i..2 * i + 2], 16).unwrap(); + } + + println!("buffer: {:?}", buffer); + buffer + } + + fn send_str(&self, buffer: &str) -> anyhow::Result<()> { + println!("------"); + for line in buffer.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("//") { + continue; + } + + let buffer = Self::line_to_bytes(line); + self.device.send_feature_report(&buffer)?; + } + Ok(()) + } + + const NB_LEDS_PER_PACKET: usize = 19; + + fn set_color_device(&self, color: &RGB, device: u8, nb_leds: usize) -> anyhow::Result<()> { + let nb_packets = (nb_leds - 1) / Self::NB_LEDS_PER_PACKET + 1; + for i in 0..nb_packets { + let mut buffer = [0u8; 64]; + let nb_leds_in_packet = if i == nb_packets - 1 && nb_leds % Self::NB_LEDS_PER_PACKET > 0 + { + nb_leds % Self::NB_LEDS_PER_PACKET + } else { + Self::NB_LEDS_PER_PACKET + }; + + buffer[0] = 0xCC; + buffer[1] = device; + buffer[2] = (i * Self::NB_LEDS_PER_PACKET) as u8 * 3; + buffer[4] = nb_leds_in_packet as u8 * 3; + + for j in 0..nb_leds_in_packet { + buffer[5 + 3 * j] = color.green; + buffer[5 + 3 * j + 1] = color.red; + buffer[5 + 3 * j + 2] = color.blue; + } + + // println!("BUFFER: {:?}", buffer); // Debug. + self.device.send_feature_report(&buffer)?; + } + + Ok(()) + } + + fn set_color_motherboard(&self, color: &RGB) -> anyhow::Result<()> { + { + let mut buffer = [0u8; 64]; + buffer[0] = 0xCC; + buffer[1] = 0x22; + buffer[2] = 0x04; + + buffer[11] = 0x01; + buffer[12] = 0x5a; + + buffer[14] = color.blue; + buffer[15] = color.green; + buffer[16] = color.red; + + self.device.send_feature_report(&buffer)?; + } + { + let mut buffer = [0u8; 64]; + buffer[0] = 0xCC; + buffer[1] = 0x28; + buffer[2] = 0xFF; + buffer[3] = 0x07; + + self.device.send_feature_report(&buffer)?; + } + + Ok(()) + } + + pub fn set_color(&self, color: &RGB) { + // Motherboard & GPU power cables. + self.set_color_device(color, 0x58, 19).unwrap(); + + // Arctic freezer 3. + self.set_color_device(color, 0x62, 38).unwrap(); + + self.set_color_motherboard(color).unwrap(); + } +} diff --git a/src/lian_li_sl_infinity.rs b/src/lian_li_sl_infinity.rs index d16a4a3..cd3e149 100644 --- a/src/lian_li_sl_infinity.rs +++ b/src/lian_li_sl_infinity.rs @@ -25,11 +25,9 @@ pub struct Device { impl Device { pub fn new(api: &hidapi::HidApi) -> Self { - let device = Device { + Self { device: api.open(LIANLI_VID, LIANLI_UNI_HUB_SLINF_PID).unwrap(), - }; - - device + } } fn send_start_action(&self, channel_id: u8) { diff --git a/src/machine/jiji.rs b/src/machine/jiji.rs new file mode 100644 index 0000000..402d9a4 --- /dev/null +++ b/src/machine/jiji.rs @@ -0,0 +1,57 @@ +use crate::{asus_aura_usb, corsair_vengeance, cpu_temperature, rgb}; + +use super::Machine; + +pub struct MachineJiji { + ram: Vec, + b650e_device: asus_aura_usb::Device, + // a770: a770::A770, + // gpu_devices: intel_arc::Devices, + gpus: Vec, +} + +impl MachineJiji { + pub fn new() -> anyhow::Result { + let api = hidapi::HidApi::new().unwrap(); + Ok(MachineJiji { + ram: vec![ + corsair_vengeance::Controller::new(0x19), + corsair_vengeance::Controller::new(0x1B), + ], + b650e_device: asus_aura_usb::Device::new(&api, asus_aura_usb::Motherboard::Asus650e)?, + // a770: a770::A770::new()?, + // gpu_devices: unsafe { intel_arc::GetDevices() }, + gpus: nvapi::PhysicalGpu::enumerate()?, + }) + } +} + +impl Machine for MachineJiji { + fn set_color_1(&mut self, color: &rgb::RGB) { + for controller in &self.ram { + controller.set_color(color); + } + self.b650e_device.set_color(color).unwrap(); + } + + fn set_color_2(&mut self, color: &rgb::RGB) {} // No color 2. + + fn get_gpu_tmp(&self) -> f32 { + // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } + self.gpus[0].thermal_settings(None).unwrap()[0] + .current_temperature + .0 as f32 + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} + +// impl Drop for MachineJiji { +// fn drop(&mut self) { +// unsafe { +// intel_arc::FreeDevices(self.gpu_devices); +// } +// } +// } diff --git a/src/machine.rs b/src/machine/lyss_metal.rs similarity index 54% rename from src/machine.rs rename to src/machine/lyss_metal.rs index 1d7bb2e..ba4cc43 100644 --- a/src/machine.rs +++ b/src/machine/lyss_metal.rs @@ -1,80 +1,8 @@ -use log::error; use nvapi::sys::i2c; -use crate::{ - /*a770,*/ asus_aura_usb, corsair_lighting_pro, corsair_vengeance, cpu_temperature, - intel_arc, lian_li_sl_infinity, rgb, -}; +use crate::{asus_aura_usb, corsair_lighting_pro, cpu_temperature, lian_li_sl_infinity, rgb}; -const RGB_FUSION2_GPU_REG_COLOR: u8 = 0x40; -const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; - -const GIGABYTE_RTX3080TI_VISION_OC_ADDR: u8 = 0x63; - -pub trait Machine { - fn set_color(&mut self, color: &rgb::RGB) { - self.set_color_1(&color); - self.set_color_2(&color); - } - fn set_color_1(&mut self, color: &rgb::RGB); - fn set_color_2(&mut self, color: &rgb::RGB); - fn get_gpu_tmp(&self) -> f32; - fn get_cpu_tmp(&self) -> f32; -} - -pub struct MachineJiji { - ram: Vec, - b650e_device: asus_aura_usb::Device, - // a770: a770::A770, - // gpu_devices: intel_arc::Devices, - gpus: Vec, -} - -impl MachineJiji { - pub fn new() -> anyhow::Result { - let api = hidapi::HidApi::new().unwrap(); - Ok(MachineJiji { - ram: vec![ - corsair_vengeance::Controller::new(0x19), - corsair_vengeance::Controller::new(0x1B), - ], - b650e_device: asus_aura_usb::Device::new(&api, asus_aura_usb::Motherboard::Asus650e)?, - // a770: a770::A770::new()?, - // gpu_devices: unsafe { intel_arc::GetDevices() }, - gpus: nvapi::PhysicalGpu::enumerate()?, - }) - } -} - -impl Machine for MachineJiji { - fn set_color_1(&mut self, color: &rgb::RGB) { - for controller in &self.ram { - controller.set_color(&color); - } - self.b650e_device.set_color(&color).unwrap(); - } - - fn set_color_2(&mut self, color: &rgb::RGB) {} // No color 2. - - fn get_gpu_tmp(&self) -> f32 { - // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } - self.gpus[0].thermal_settings(None).unwrap()[0] - .current_temperature - .0 as f32 - } - - fn get_cpu_tmp(&self) -> f32 { - cpu_temperature::read() - } -} - -// impl Drop for MachineJiji { -// fn drop(&mut self) { -// unsafe { -// intel_arc::FreeDevices(self.gpu_devices); -// } -// } -// } +use super::Machine; pub struct MachineLyssMetal { crosshair_device: asus_aura_usb::Device, @@ -89,7 +17,7 @@ impl MachineLyssMetal { nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); - let machine = MachineLyssMetal { + let machine = Self { crosshair_device: asus_aura_usb::Device::new( &api, asus_aura_usb::Motherboard::AsusCrosshairVIIIHero, @@ -117,6 +45,7 @@ impl MachineLyssMetal { // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp // * i2c_smbus\i2c_smbus_nvapi.cpp // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 + // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html pub fn test_i2c(&self) { // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; @@ -125,7 +54,7 @@ impl MachineLyssMetal { 0, Some(1), false, - GIGABYTE_RTX3080TI_VISION_OC_ADDR, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, &[], &data, i2c::I2cSpeed::Default, @@ -135,7 +64,7 @@ impl MachineLyssMetal { fn set_mode_3080ti(&self) { let data = [ - RGB_FUSION2_GPU_REG_MODE, + super::RGB_FUSION2_GPU_REG_MODE, 0x01, // Mode (1: static). 0x00, // Speed. 0x63, // Brightness max. @@ -149,7 +78,7 @@ impl MachineLyssMetal { 0, Some(1), false, - GIGABYTE_RTX3080TI_VISION_OC_ADDR, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, &[], &data, i2c::I2cSpeed::Default, @@ -165,13 +94,13 @@ impl MachineLyssMetal { impl Machine for MachineLyssMetal { fn set_color_1(&mut self, color: &rgb::RGB) { - self.crosshair_device.set_color(&color).unwrap(); - self.corsair_lignting_pro.set_color(&color); + self.crosshair_device.set_color(color).unwrap(); + self.corsair_lignting_pro.set_color(color); // self.set_color_3080ti(&color); // TODO. } fn set_color_2(&mut self, color: &rgb::RGB) { - self.lian_li_sl_infinity.set_color(&color); + self.lian_li_sl_infinity.set_color(color); } fn get_gpu_tmp(&self) -> f32 { diff --git a/src/machine/lyss_metal2.rs b/src/machine/lyss_metal2.rs new file mode 100644 index 0000000..f96d213 --- /dev/null +++ b/src/machine/lyss_metal2.rs @@ -0,0 +1,114 @@ +use nvapi::sys::i2c; + +use crate::{ + corsair_lighting_pro, cpu_temperature, gigabyte_rgb_fusion_usb, lian_li_sl_infinity, rgb, +}; + +use super::Machine; + +pub struct MachineLyssMetal2 { + fusion_device: gigabyte_rgb_fusion_usb::Device, + corsair_lignting_pro: corsair_lighting_pro::Device, + lian_li_sl_infinity: lian_li_sl_infinity::Device, + gpus: Vec, +} + +impl MachineLyssMetal2 { + pub fn new() -> anyhow::Result { + let api = hidapi::HidApi::new()?; + + nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); + + let machine = Self { + fusion_device: gigabyte_rgb_fusion_usb::Device::new(&api)?, + corsair_lignting_pro: corsair_lighting_pro::Device::new( + &api, + &rgb::RGB { + red: 0, + green: 255, + blue: 40, + }, + ), + lian_li_sl_infinity: lian_li_sl_infinity::Device::new(&api), + gpus: nvapi::PhysicalGpu::enumerate()?, + }; + + // machine.set_mode_3080ti(); + Ok(machine) + } + + // Doesn't work: "Error: NotSupported". + // From OpenRGB, see the following files: + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUControllerDetect.cpp + // * Controllers\GigabyteRGBFusion2GPUController\RGBController_GigabyteRGBFusion2GPU.cpp + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp + // * i2c_smbus\i2c_smbus_nvapi.cpp + // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 + // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html + pub fn test_i2c(&self) { + // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' + let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_mode_3080ti(&self) { + let data = [ + super::RGB_FUSION2_GPU_REG_MODE, + 0x01, // Mode (1: static). + 0x00, // Speed. + 0x63, // Brightness max. + 0x00, // Mistery flag. + 0x01, // Zone. + 0x00, + 0x00, + ]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_color_3080ti(&self, color: &rgb::RGB) { + // TODO. + self.test_i2c(); + } +} + +impl Machine for MachineLyssMetal2 { + fn set_color_1(&mut self, color: &rgb::RGB) { + self.corsair_lignting_pro.set_color(color); + self.fusion_device.set_color(color); + // self.set_color_3080ti(&color); // TODO. + } + + fn set_color_2(&mut self, color: &rgb::RGB) { + self.lian_li_sl_infinity.set_color(color); + } + + fn get_gpu_tmp(&self) -> f32 { + self.gpus[0].thermal_settings(None).unwrap()[0] + .current_temperature + .0 as f32 + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} diff --git a/src/machine/mod.rs b/src/machine/mod.rs new file mode 100644 index 0000000..0ed2ff5 --- /dev/null +++ b/src/machine/mod.rs @@ -0,0 +1,21 @@ +use crate::rgb; + +pub mod jiji; +pub mod lyss_metal; +pub mod lyss_metal2; + +const RGB_FUSION2_GPU_REG_COLOR: u8 = 0x40; +const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; + +const GIGABYTE_RTX3080TI_VISION_OC_ADDR: u8 = 0x63; + +pub trait Machine { + fn set_color(&mut self, color: &rgb::RGB) { + self.set_color_1(color); + self.set_color_2(color); + } + fn set_color_1(&mut self, color: &rgb::RGB); + fn set_color_2(&mut self, color: &rgb::RGB); + fn get_gpu_tmp(&self) -> f32; + fn get_cpu_tmp(&self) -> f32; +} diff --git a/src/main.rs b/src/main.rs index a0443f7..ff87264 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,7 @@ mod intel_arc { mod a770; mod asus_aura_usb; mod corsair_lighting_pro; +mod gigabyte_rgb_fusion_usb; mod lian_li_sl_infinity; mod machine; mod main_loop; @@ -183,7 +184,7 @@ fn run_service(_arguments: Vec) -> Result<(), windows_service::Error> let completed_event_handler = Arc::clone(&completed); - info!("Setuping the event handler..."); + info!("Setup the event handler..."); let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { diff --git a/src/main_loop.rs b/src/main_loop.rs index a11c36b..1d523b3 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -35,15 +35,20 @@ pub fn main_loop(completed: Arc) { }; let settings = settings::Settings::read(&file_conf_path).expect("Cannot load settings"); - println!("Settings: {settings:?}"); + println!("Settings: {settings:?} from {file_conf_path}"); let mut machine: Box = match settings.machine_name { settings::MachineName::Jiji => { - Box::new(machine::MachineJiji::new().expect("Unable to create MachineJiji")) - } - settings::MachineName::LyssMetal => { - Box::new(machine::MachineLyssMetal::new().expect("Unable to create MachineLyssMetal")) + Box::new(machine::jiji::MachineJiji::new().expect("Unable to create MachineJiji")) } + settings::MachineName::LyssMetal => Box::new( + machine::lyss_metal::MachineLyssMetal::new() + .expect("Unable to create MachineLyssMetal"), + ), + settings::MachineName::LyssMetal2 => Box::new( + machine::lyss_metal2::MachineLyssMetal2::new() + .expect("Unable to create MachineLyssMetal2"), + ), }; let mut kernel = [0f32; consts::KERNEL_SIZE_SAMPLES]; diff --git a/src/piix4_i2c.rs b/src/piix4_i2c.rs index 7acc572..7397459 100644 --- a/src/piix4_i2c.rs +++ b/src/piix4_i2c.rs @@ -88,18 +88,17 @@ impl I2c { ); let mut data_block = [0u8; I2C_BLOCK_MAX + 2]; data_block[0] = l as u8; - data_block[1..l + 1].copy_from_slice(&data); + data_block[1..l + 1].copy_from_slice(data); unsafe { - match self.i2c_smbus_xfer( + if let Err(error) = self.i2c_smbus_xfer( addr, AccessType::Write, command, TransactionType::I2cSmbusBlockData, Some(&data_block), ) { - Err(error) => println!("Error when writing block (I2c): {error:?}"), - Ok(_) => (), + println!("Error when writing block (I2c): {error:?}"); } } } @@ -129,7 +128,7 @@ impl I2c { TransactionType::I2cSmbusQuick => { self.write_io_port_byte( SMBusAddressOffsets::Smbhstadd, - addr << 1 | access_type as u8, + (addr << 1) | access_type as u8, ); Piix4TransactionType::Piix4Quick } @@ -139,7 +138,7 @@ impl I2c { TransactionType::I2cSmbusBlockData => { self.write_io_port_byte( SMBusAddressOffsets::Smbhstadd, - addr << 1 | access_type as u8, + (addr << 1) | access_type as u8, ); self.write_io_port_byte(SMBusAddressOffsets::Smbhstcmd, command); if let AccessType::Write = access_type { @@ -188,7 +187,7 @@ impl I2c { for i in 1..=l { data[i] = self.read_io_port_byte(SMBusAddressOffsets::Smbblkdat); } - return Ok(XferResult::BlockData(data)); + Ok(XferResult::BlockData(data)) } } } @@ -237,7 +236,7 @@ impl I2c { self.write_io_port_byte(SMBusAddressOffsets::Smbhststs, res); } - return Ok(()); + Ok(()) } unsafe fn write_io_port_byte(&self, op: SMBusAddressOffsets, value: u8) { diff --git a/src/settings.rs b/src/settings.rs index 511cb8c..43ae03d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -12,6 +12,7 @@ use crate::rgb::RGB; pub enum MachineName { Jiji, LyssMetal, + LyssMetal2, } #[derive(Debug, Deserialize, Serialize)] diff --git a/src/tests.rs b/src/tests.rs index b49081e..c7ea81c 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,7 +4,7 @@ use wmi::{COMLibrary, Variant, WMIConnection}; use crate::{ a770, asus_aura_usb, corsair_lighting_pro, corsair_vengeance, cpu_temperature, - lian_li_sl_infinity, machine, rgb::RGB, winring0, wrapper_winring0, + gigabyte_rgb_fusion_usb, lian_li_sl_infinity, machine, rgb::RGB, winring0, wrapper_winring0, }; pub fn tests() { @@ -14,7 +14,7 @@ pub fn tests() { // test_asus_aura_usb(asus_aura_usb::Motherboard::Asus650e); // test_corsair_lighting_pro(); - test_lianli_sl_infinity(); + // test_lianli_sl_infinity(); // list_usb_devices(); // test_roccat(); // test_wmi(); @@ -24,6 +24,7 @@ pub fn tests() { // test_read_temperature_cpu(); // test_read_temperature_a770(); // test_read_temperature_3080(); + test_gigabyte_fusion(); winring0::deinit(); @@ -31,6 +32,17 @@ pub fn tests() { std::io::stdin().read_line(&mut String::new()).unwrap(); } +fn test_gigabyte_fusion() { + let api = hidapi::HidApi::new().unwrap(); + let device = gigabyte_rgb_fusion_usb::Device::new(&api).unwrap(); + // device.test_raw_data().unwrap(); + device.set_color(&RGB { + red: 0xFF, + green: 0x00, + blue: 0x00, + }); +} + fn test_wmi() { let com_con = COMLibrary::new().unwrap(); let wmi_con = WMIConnection::new(com_con.into()).unwrap(); @@ -170,7 +182,8 @@ fn test_a770() { } fn test_3080ti() { - let machine: &mut dyn machine::Machine = &mut machine::MachineLyssMetal::new().unwrap(); + let machine: &mut dyn machine::Machine = + &mut machine::lyss_metal::MachineLyssMetal::new().unwrap(); machine.set_color(&RGB { red: 255, @@ -188,7 +201,7 @@ fn test_read_temperature_cpu() { } fn test_read_temperature_a770() { - let jiji: &dyn machine::Machine = &machine::MachineJiji::new().unwrap(); + let jiji: &dyn machine::Machine = &machine::jiji::MachineJiji::new().unwrap(); println!("temp gpu: {}", jiji.get_gpu_tmp()); } -- 2.50.0 From f215b5b6bdc772bfc91d54640a468fa19f0145d5 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Wed, 19 Mar 2025 18:16:03 +0100 Subject: [PATCH 4/6] Remove a lot of 'unwrap' --- Cargo.toml | 129 ++++---- build.rs | 179 +++++------ src/a770.rs | 154 ++++----- src/asus_aura_usb.rs | 26 +- src/common.rs | 0 src/corsair_lighting_pro.rs | 108 ++++--- src/corsair_vengeance.rs | 204 ++++++------ src/gigabyte_rgb_fusion_usb.rs | 19 +- src/lian_li_sl_infinity.rs | 176 ++++++----- src/machine/jiji.rs | 117 +++---- src/machine/lyss_metal.rs | 232 +++++++------- src/machine/lyss_metal2.rs | 231 +++++++------- src/machine/mod.rs | 43 +-- src/main.rs | 563 +++++++++++++++++++-------------- src/main_loop.rs | 19 +- src/piix4_i2c.rs | 547 ++++++++++++++++---------------- src/rgb.rs | 34 +- src/settings.rs | 128 ++++---- src/tests.rs | 451 +++++++++++++------------- 19 files changed, 1741 insertions(+), 1619 deletions(-) delete mode 100644 src/common.rs diff --git a/Cargo.toml b/Cargo.toml index ce95993..d14eea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,63 +1,66 @@ -[package] -name = "temp_2_rgb" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -# [build] -# target = "i686-pc-windows-msvc" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -# Rust object notation, to load configuration files. -ron = "0.8" - -num = "0.4" - -dirs = "6.0" -anyhow = "1.0" - -flexi_logger = "0.29" -log-panics = { version = "2", features = ["with-backtrace"] } -log = "0.4" - -windows-service = "0.7" - -# HIDAPI is a library which allows an application to interface with -# USB and Bluetooth HID-Class devices. -hidapi = "2.6" - -# Nvidia API. -nvapi = "0.1" - -libc = "0.2" -wmi = "0.15" -crc = "3.2" - -# libloading = "0.8" -# netcorehost = "0.15" - -[dependencies.windows] -version = "0.59" -features = [ - "Win32_Foundation", - "Win32_Security", - "Win32_Storage_FileSystem", - "Win32_System_IO", - "Win32_System_Services", - "Win32_System_LibraryLoader", - "Win32_System_Threading", - # "Devices_I2c", - # "Devices_Enumeration", - # "Foundation", - # "Foundation_Collections", -] - -[build-dependencies] -bindgen = "0.71" - -[profile.release] -# strip = "debuginfo" -codegen-units = 1 -lto = true +[package] +name = "temp_2_rgb" +version = "0.1.0" +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +# [build] +# target = "i686-pc-windows-msvc" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +# Rust object notation, to load configuration files. +ron = "0.8" + +num = "0.4" + +dirs = "6.0" +anyhow = "1.0" + +clap = { version = "4", features = ["derive"] } + +flexi_logger = "0.29" +log-panics = { version = "2", features = ["with-backtrace"] } +log = "0.4" + +windows-service = "0.8" + +# HIDAPI is a library which allows an application to interface with +# USB and Bluetooth HID-Class devices. +hidapi = "2.6" + +# Nvidia API. +nvapi = "0.1" + +libc = "0.2" +wmi = "0.15" +crc = "3.2" + +# libloading = "0.8" +# netcorehost = "0.15" + +[dependencies.windows] +version = "0.60" +features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Services", + "Win32_System_LibraryLoader", + "Win32_System_Threading", + "Win32_System_SystemInformation", + # "Devices_I2c", + # "Devices_Enumeration", + # "Foundation", + # "Foundation_Collections", +] + +[build-dependencies] +bindgen = "0.71" + +[profile.release] +# strip = "debuginfo" +codegen-units = 1 +lto = true diff --git a/build.rs b/build.rs index 6612712..59d15d9 100644 --- a/build.rs +++ b/build.rs @@ -1,89 +1,90 @@ -extern crate bindgen; - -use std::{env, path::PathBuf}; - -// From: https://rust-lang.github.io/rust-bindgen/tutorial-0.html - -fn main() { - // Tell cargo to look for shared libraries in the specified directory - println!("cargo:rustc-link-search=winring0"); - println!("cargo:rustc-link-search=IntelArc"); - - // Tell cargo to tell rustc to link the system 'WinRing0x64' shared library. - println!("cargo:rustc-link-lib=WinRing0x64"); - println!("cargo:rustc-link-lib=IntelArc"); - - // Tell cargo to invalidate the built crate whenever the header changes - println!("cargo:rerun-if-changed=OlsApi.h"); - println!("cargo:rerun-if-changed=IntelArc.h"); - - // The bindgen::Builder is the main entry point - // to bindgen, and lets you build up options for - // the resulting bindings. - let bindings_winring0 = bindgen::Builder::default() - // The input header we would like to generate bindings for. - .header("winring0/OlsApi.h") - // .clang_arg("-target") - // .clang_arg("i686-pc-windows-msvc") - .clang_arg("-x") - .clang_arg("c++") - .clang_arg("--std") - .clang_arg("c++14") - // Commented out: not needed. - // Tell cargo to invalidate the built crate whenever any of the - // included header files changed. - //.parse_callbacks(Box::new(bindgen::CargoCallbacks)) - // Finish the builder and generate the bindings. - .generate() - // Unwrap the Result and panic on failure. - .expect("Unable to generate bindings for winring0"); - - // Write the bindings to the $OUT_DIR/bindings.rs file. - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - - bindings_winring0 - .write_to_file(out_path.join("ols_api.rs")) - .expect("Couldn't write bindings for winring0!"); - - // The bindgen::Builder is the main entry point - // to bindgen, and lets you build up options for - // the resulting bindings. - let bindings_intel_arc = bindgen::Builder::default() - // The input header we would like to generate bindings for. - .header("IntelArc/IntelArc.h") - // .clang_arg("-target") - // .clang_arg("i686-pc-windows-msvc") - .clang_arg("-x") - .clang_arg("c++") - .clang_arg("--std") - .clang_arg("c++14") - // Commented out: not needed. - // Tell cargo to invalidate the built crate whenever any of the - // included header files changed. - //.parse_callbacks(Box::new(bindgen::CargoCallbacks)) - // Finish the builder and generate the bindings. - .generate() - // Unwrap the Result and panic on failure. - .expect("Unable to generate bindings for IntelArc"); - - // Write the bindings to the $OUT_DIR/bindings.rs file. - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - - bindings_intel_arc - .write_to_file(out_path.join("intel_arc.rs")) - .expect("Couldn't write bindings for intel arc!"); - - // let out_dir = env::var("CARGO_TARGET_DIR").unwrap(); - // println!("out_dir: {}", out_dir); - // TODO: How to properly get the (current) target directory? - copy_file("winring0/WinRing0x64.sys", "target/debug/WinRing0x64.sys"); - copy_file("winring0/WinRing0x64.dll", "target/debug/WinRing0x64.dll"); - copy_file("winring0/WinRing0x64.sys", "target/release/WinRing0x64.sys"); - copy_file("winring0/WinRing0x64.dll", "target/release/WinRing0x64.dll"); -} - -fn copy_file(from: &str, to: &str) { - if let Err(e) = std::fs::copy(from, to) { - println!("cargo:warning={e:?} (copy {from} to {to})") - }; -} +extern crate bindgen; + +use std::{env, path::PathBuf}; + +// From: https://rust-lang.github.io/rust-bindgen/tutorial-0.html + +fn main() { + // Tell cargo to look for shared libraries in the specified directory + println!("cargo:rustc-link-search=winring0"); + // println!("cargo:rustc-link-search=IntelArc"); + + // Tell cargo to tell rustc to link the system 'WinRing0x64' shared library. + println!("cargo:rustc-link-lib=WinRing0x64"); + // println!("cargo:rustc-link-lib=IntelArc"); + + // Tell cargo to invalidate the built crate whenever the header changes + println!("cargo:rerun-if-changed=OlsApi.h"); + // println!("cargo:rerun-if-changed=IntelArc.h"); + + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings_winring0 = bindgen::Builder::default() + // The input header we would like to generate bindings for. + .header("winring0/OlsApi.h") + // .clang_arg("-target") + // .clang_arg("i686-pc-windows-msvc") + .clang_arg("-x") + .clang_arg("c++") + .clang_arg("--std") + .clang_arg("c++14") + // Commented out: not needed. + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + //.parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + .expect("Unable to generate bindings for winring0"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + + bindings_winring0 + .write_to_file(out_path.join("ols_api.rs")) + .expect("Couldn't write bindings for winring0!"); + + /* + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings_intel_arc = bindgen::Builder::default() + // The input header we would like to generate bindings for. + .header("IntelArc/IntelArc.h") + // .clang_arg("-target") + // .clang_arg("i686-pc-windows-msvc") + .clang_arg("-x") + .clang_arg("c++") + .clang_arg("--std") + .clang_arg("c++14") + // Commented out: not needed. + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + //.parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings for IntelArc"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + + bindings_intel_arc + .write_to_file(out_path.join("intel_arc.rs")) + .expect("Couldn't write bindings for intel arc!"); + */ + + // let out_dir = env::var("CARGO_TARGET_DIR").unwrap(); + // println!("out_dir: {}", out_dir); + // TODO: How to properly get the (current) target directory? + copy_file("winring0/WinRing0x64.sys", "target/debug/WinRing0x64.sys"); + copy_file("winring0/WinRing0x64.dll", "target/debug/WinRing0x64.dll"); + copy_file("winring0/WinRing0x64.sys", "target/release/WinRing0x64.sys"); + copy_file("winring0/WinRing0x64.dll", "target/release/WinRing0x64.dll"); +} + +fn copy_file(from: &str, to: &str) { + if let Err(e) = std::fs::copy(from, to) { + println!("cargo:warning={e:?} (copy {from} to {to})") + }; +} diff --git a/src/a770.rs b/src/a770.rs index ed6c18e..8edfcb6 100644 --- a/src/a770.rs +++ b/src/a770.rs @@ -1,77 +1,77 @@ -// use windows::{ -// core::w, -// Win32::{self, Storage::FileSystem}, -// }; -// use netcorehost::{nethost, pdcstr}; - -// pub fn set_rgb(r: u8, g: u8, b: u8) { -// unsafe { -// let lib = libloading::Library::new("IntelOCWrapper.dll").unwrap(); - -// let fun: libloading::Symbol bool> = lib.get(b"SetLEDColor").unwrap(); -// let ctlInit: libloading::Symbol std::ffi::c_void> = lib.get(b"ctlInit").unwrap(); -// let ctlInit: libloading::Symbol std::ffi::c_void> = lib.get(b"SetLEDColor").unwrap(); -// println!("ok"); -// } - -// let hostfxr = nethost::load_hostfxr().unwrap(); -// let context = hostfxr.initialize_for_dotnet_command_line(pdcstr!("IntelOCWrapper.dll")).unwrap(); -// let result = context.run_app().value(); - -// unsafe { -// let handle = FileSystem::CreateFileW( -// // w!("\\\\.\\Intel_NF_I2C"), -// w!("\\\\.\\VIDEO\\INTC_I2C"), -// // w!("\\\\.\\WinRing0_1_2_0"), -// 3221225472, -// FileSystem::FILE_SHARE_MODE(0), -// None, -// FileSystem::FILE_CREATION_DISPOSITION(3), -// FileSystem::FILE_FLAGS_AND_ATTRIBUTES(1073741824), -// Win32::Foundation::HANDLE::default(), -// ); - -// println!("handle: {:?}", handle); -// } - -//"\\\\.\\Intel_NF_I2C" -// } - -// internal static \u0024ArrayType\u0024\u0024\u0024BY08E \u003FA0x171ed149\u002E\u003FprevData\u0040\u003F1\u003F\u003FSetLEDBehavior\u0040CVGAAdaptor\u0040\u0040UEAA_NEEEEEEEEE\u0040Z\u00404PAEA; -// public static __FnPtr<_ctl_result_t (_ctl_init_args_t*, _ctl_api_handle_t**)> __m2mep\u0040\u003FctlInit\u0040\u0040\u0024\u0024J0YA\u003FAW4_ctl_result_t\u0040\u0040PEAU_ctl_init_args_t\u0040\u0040PEAPEAU_ctl_api_handle_t\u0040\u0040\u0040Z; - -use log::error; - -use std::{ - io::prelude::*, - net::TcpStream, - process::{Child, Command}, -}; - -pub struct A770 { - process: Child, - stream: TcpStream, -} - -impl A770 { - pub fn new() -> anyhow::Result { - Ok(A770 { - process: Command::new(r"IntelOC.exe").spawn()?, - stream: TcpStream::connect("127.0.0.1:6577")?, - }) - } - - pub fn set_color(&mut self, r: u8, g: u8, b: u8) -> anyhow::Result<()> { - let buffer: [u8; 3] = [r, g, b]; - self.stream.write(&buffer).map(|_| ())?; - Ok(()) - } -} - -impl Drop for A770 { - fn drop(&mut self) { - if let Err(error) = self.process.kill().and(self.process.try_wait()) { - error!("Unable to kill the child process: {:?}", error); - } - } -} +// use windows::{ +// core::w, +// Win32::{self, Storage::FileSystem}, +// }; +// use netcorehost::{nethost, pdcstr}; + +// pub fn set_rgb(r: u8, g: u8, b: u8) { +// unsafe { +// let lib = libloading::Library::new("IntelOCWrapper.dll").unwrap(); + +// let fun: libloading::Symbol bool> = lib.get(b"SetLEDColor").unwrap(); +// let ctlInit: libloading::Symbol std::ffi::c_void> = lib.get(b"ctlInit").unwrap(); +// let ctlInit: libloading::Symbol std::ffi::c_void> = lib.get(b"SetLEDColor").unwrap(); +// println!("ok"); +// } + +// let hostfxr = nethost::load_hostfxr().unwrap(); +// let context = hostfxr.initialize_for_dotnet_command_line(pdcstr!("IntelOCWrapper.dll")).unwrap(); +// let result = context.run_app().value(); + +// unsafe { +// let handle = FileSystem::CreateFileW( +// // w!("\\\\.\\Intel_NF_I2C"), +// w!("\\\\.\\VIDEO\\INTC_I2C"), +// // w!("\\\\.\\WinRing0_1_2_0"), +// 3221225472, +// FileSystem::FILE_SHARE_MODE(0), +// None, +// FileSystem::FILE_CREATION_DISPOSITION(3), +// FileSystem::FILE_FLAGS_AND_ATTRIBUTES(1073741824), +// Win32::Foundation::HANDLE::default(), +// ); + +// println!("handle: {:?}", handle); +// } + +//"\\\\.\\Intel_NF_I2C" +// } + +// internal static \u0024ArrayType\u0024\u0024\u0024BY08E \u003FA0x171ed149\u002E\u003FprevData\u0040\u003F1\u003F\u003FSetLEDBehavior\u0040CVGAAdaptor\u0040\u0040UEAA_NEEEEEEEEE\u0040Z\u00404PAEA; +// public static __FnPtr<_ctl_result_t (_ctl_init_args_t*, _ctl_api_handle_t**)> __m2mep\u0040\u003FctlInit\u0040\u0040\u0024\u0024J0YA\u003FAW4_ctl_result_t\u0040\u0040PEAU_ctl_init_args_t\u0040\u0040PEAPEAU_ctl_api_handle_t\u0040\u0040\u0040Z; + +use log::error; + +use std::{ + io::prelude::*, + net::TcpStream, + process::{Child, Command}, +}; + +pub struct A770 { + process: Child, + stream: TcpStream, +} + +impl A770 { + pub fn new() -> anyhow::Result { + Ok(A770 { + process: Command::new(r"IntelOC.exe").spawn()?, + stream: TcpStream::connect("127.0.0.1:6577")?, + }) + } + + pub fn set_color(&mut self, r: u8, g: u8, b: u8) -> anyhow::Result<()> { + let buffer: [u8; 3] = [r, g, b]; + self.stream.write(&buffer).map(|_| ())?; + Ok(()) + } +} + +impl Drop for A770 { + fn drop(&mut self) { + if let Err(error) = self.process.kill().and(self.process.try_wait()) { + error!("Unable to kill the child process: {:?}", error); + } + } +} diff --git a/src/asus_aura_usb.rs b/src/asus_aura_usb.rs index 31a2b27..2f8591d 100644 --- a/src/asus_aura_usb.rs +++ b/src/asus_aura_usb.rs @@ -1,6 +1,6 @@ use std::str; -use crate::rgb::RGB; +use crate::rgb::Rgb; /* * Doc: @@ -48,13 +48,13 @@ impl Device { buffer[0] = 0xEC; buffer[1] = AURA_REQUEST_FIRMWARE_VERSION; let n_write = self.device.write(&buffer)?; - assert_eq!(n_write, 65); + // assert_eq!(n_write, 65); buffer.fill(0); let n_read = self.device.read(&mut buffer)?; - assert_eq!(n_read, 65); - assert_eq!(buffer[0], 0xEC); - assert_eq!(buffer[1], 0x02); + // assert_eq!(n_read, 65); + // assert_eq!(buffer[0], 0xEC); + // assert_eq!(buffer[1], 0x02); Ok(String::from(str::from_utf8(&buffer[2..17])?)) } @@ -79,13 +79,13 @@ impl Device { buffer[0] = 0xEC; buffer[1] = AURA_REQUEST_CONFIG_TABLE; let n_write = self.device.write(&buffer)?; - assert_eq!(n_write, 65); + // assert_eq!(n_write, 65); buffer.fill(0); let n_read = self.device.read(&mut buffer)?; - assert_eq!(n_read, 65); - assert_eq!(buffer[0], 0xEC); - assert_eq!(buffer[1], 0x30); + // assert_eq!(n_read, 65); + // assert_eq!(buffer[0], 0xEC); + // assert_eq!(buffer[1], 0x30); Ok(buffer[4..64].try_into()?) } @@ -100,12 +100,12 @@ impl Device { for channel_effect_id in 0..2 { buffer[2] = channel_effect_id; // Channel effect id: Fixed. let n_write = self.device.write(&buffer)?; - assert_eq!(n_write, 65); + // assert_eq!(n_write, 65); } Ok(()) } - pub fn set_color(&self, color: &RGB) -> anyhow::Result<()> { + pub fn set_color(&self, color: &Rgb) -> anyhow::Result<()> { let mut buffer = [0u8; 65]; buffer[0] = 0xEC; buffer[1] = 0x36; @@ -124,7 +124,7 @@ impl Device { } let n_write = self.device.write(&buffer)?; - assert_eq!(n_write, 65); + // assert_eq!(n_write, 65); Ok(()) } @@ -135,7 +135,7 @@ impl Device { buffer[2] = 0x55; let n_write = self.device.write(&buffer)?; - assert_eq!(n_write, 65); + // assert_eq!(n_write, 65); Ok(()) } } diff --git a/src/common.rs b/src/common.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/corsair_lighting_pro.rs b/src/corsair_lighting_pro.rs index 25bee13..957326b 100644 --- a/src/corsair_lighting_pro.rs +++ b/src/corsair_lighting_pro.rs @@ -1,4 +1,4 @@ -use crate::rgb::RGB; +use crate::rgb::Rgb; const CORSAIR_VID: u16 = 0x1B1C; const CORSAIR_LIGHTING_NODE_PRO_PID: u16 = 0x0C0B; @@ -32,65 +32,69 @@ pub struct Device { } impl Device { - pub fn new(api: &hidapi::HidApi, initial_color: &RGB) -> Self { + pub fn new(api: &hidapi::HidApi, initial_color: &Rgb) -> anyhow::Result { let device = Device { - device: api - .open(CORSAIR_VID, CORSAIR_LIGHTING_NODE_PRO_PID) - .unwrap(), + device: api.open(CORSAIR_VID, CORSAIR_LIGHTING_NODE_PRO_PID)?, }; for channel_id in 0..CHANNEL_COUNT { - device.send_reset(channel_id); - device.send_begin(channel_id); - device.send_port_state(channel_id, CORSAIR_LIGHTING_NODE_PORT_STATE_HARDWARE); - device.send_effect_config(channel_id, initial_color); - device.send_commit(channel_id); + device.send_reset(channel_id)?; + device.send_begin(channel_id)?; + device.send_port_state(channel_id, CORSAIR_LIGHTING_NODE_PORT_STATE_HARDWARE)?; + device.send_effect_config(channel_id, initial_color)?; + device.send_commit(channel_id)?; } - device + Ok(device) } - fn send_reset(&self, channel_id: u8) { + fn send_reset(&self, channel_id: u8) -> anyhow::Result<()> { let mut buffer = [0u8; 65]; buffer[0x01] = CORSAIR_LIGHTING_NODE_PACKET_ID_RESET; buffer[0x02] = channel_id; - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); + + Ok(()) } - fn send_begin(&self, channel_id: u8) { + fn send_begin(&self, channel_id: u8) -> anyhow::Result<()> { let mut buffer = [0u8; 65]; buffer[0x01] = CORSAIR_LIGHTING_NODE_PACKET_ID_BEGIN; buffer[0x02] = channel_id; - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); + + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + Ok(()) } - fn send_port_state(&self, channel_id: u8, state: u8) { + fn send_port_state(&self, channel_id: u8, state: u8) -> anyhow::Result<()> { let mut buffer = [0u8; 65]; buffer[0x01] = CORSAIR_LIGHTING_NODE_PACKET_ID_PORT_STATE; buffer[0x02] = channel_id; buffer[0x03] = state; - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); + + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + Ok(()) } - fn send_effect_config(&self, channel_id: u8, color: &RGB) { + fn send_effect_config(&self, channel_id: u8, color: &Rgb) -> anyhow::Result<()> { println!("{color:?}"); let mut buffer = [0u8; 65]; @@ -111,31 +115,35 @@ impl Device { buffer[offset_color + 3 * i + 2] = color.blue; } - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); + + Ok(()) } - fn send_commit(&self, channel_id: u8) { + fn send_commit(&self, channel_id: u8) -> anyhow::Result<()> { let mut buffer = [0u8; 65]; buffer[0x01] = CORSAIR_LIGHTING_NODE_PACKET_ID_COMMIT; buffer[0x02] = 0xFF; - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); + + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + Ok(()) } - pub fn set_color(&self, color: &RGB) { + pub fn set_color(&self, color: &Rgb) -> anyhow::Result<()> { // println!("set_color: {color:?}"); for channel_id in 0..CHANNEL_COUNT { - self.send_port_state(channel_id, CORSAIR_LIGHTING_NODE_PORT_STATE_SOFTWARE); + self.send_port_state(channel_id, CORSAIR_LIGHTING_NODE_PORT_STATE_SOFTWARE)?; let mut buffer = [0u8; 65]; @@ -159,15 +167,17 @@ impl Device { buffer[0x06 + n as usize] = color_component; } - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, 65); + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, 65); - let n_read = self.device.read(&mut buffer[0..16]).unwrap(); - assert_eq!(n_read, 16); - assert_eq!(buffer[0], 0); + let n_read = self.device.read(&mut buffer[0..16])?; + // assert_eq!(n_read, 16); + // assert_eq!(buffer[0], 0); } - self.send_commit(channel_id); + self.send_commit(channel_id)?; } + + Ok(()) } } diff --git a/src/corsair_vengeance.rs b/src/corsair_vengeance.rs index 0c574f0..f1899dc 100644 --- a/src/corsair_vengeance.rs +++ b/src/corsair_vengeance.rs @@ -1,102 +1,102 @@ -use std::time::Duration; - -use crate::{piix4_i2c, rgb::RGB, timer}; - -// use windows::{*, Win32::{System::LibraryLoader::*, Foundation::HCS_E_CONNECTION_CLOSED, Security::InitializeAcl}, Devices::I2c::*, core::HSTRING}; - -use crc::{Algorithm, Crc}; - -const CRC8_ALG: Algorithm = Algorithm { - width: 8, - poly: 0x7, - init: 0x0, - refin: false, - refout: false, - xorout: 0x00, - check: 0x00, - residue: 0x00, -}; - -const BUS: i32 = 0; -const BUS_ADDRESS: i32 = 0x0B00; - -// Called "device location" in 'CorsairDominatorPlatinumController' class. -const ADDRESS_DDR_1: i32 = 0x19; -const ADDRESS_DDR_2: i32 = 0x1B; - -const CORSAIR_LED_COUNT: usize = 12; - -pub struct Controller { - bus: piix4_i2c::I2c, - ddr_address: u8, -} - -impl Controller { - pub fn new(ddr_address: u8) -> Self { - Controller { - bus: piix4_i2c::I2c::new(0x0B00), - ddr_address, - } - } - - pub fn test(&self) { - self.bus.i2c_smbus_write_quick(self.ddr_address, 0); - } - - pub fn set_color(&self, color: &RGB) { - let mut data = [0u8; CORSAIR_LED_COUNT * 3 + 2]; - data[0] = 0xC; - - for i in 0..CORSAIR_LED_COUNT { - let offset = i * 3 + 1; - data[offset] = color.red; - data[offset + 1] = color.green; - data[offset + 2] = color.blue; - } - - let crc = Crc::::new(&CRC8_ALG); - let mut digest = crc.digest(); - digest.update(&data[0..data.len() - 1]); // '-1' to not take the last byte. - data[data.len() - 1] = digest.finalize(); - - let timer = timer::Sleep::new(); - - self.bus - .write_block_data(self.ddr_address, 0x31, &data[0..piix4_i2c::I2C_BLOCK_MAX]); - timer.wait(Duration::from_micros(800)); - - self.bus - .write_block_data(self.ddr_address, 0x32, &data[piix4_i2c::I2C_BLOCK_MAX..]); - timer.wait(Duration::from_micros(200)); - } -} - -// TESTS WITH I2C from winapi: - -// let connection_settings = I2cConnectionSettings::Create(ADDRESS_DDR_1).unwrap(); - -// // For A770: "DISPLAY\\INTC_I2C\\7&3255D98A&0&UID26040" -// // "PCI\\VEN_1022&DEV_790B&SUBSYS_88771043&REV_71\\3&11583659&0&A0" - -// let selector = I2cDevice::GetDeviceSelector().unwrap(); -// println!("{:?}", selector); - -// let devices_async = Devices::Enumeration::DeviceInformation::FindAllAsync().unwrap(); // Devices::Enumeration::DeviceInformation::FindAllAsyncAqsFilter(&selector).unwrap(); -// let devices = devices_async.get().unwrap(); - -// // println!("{:?}", devices.Size()); - -// for i in 0..devices.Size().unwrap() { -// let device = devices.GetAt(i).unwrap(); -// println!("Device Name: {:?}", device.Name().unwrap()); -// println!("Device Kind: {:?}", device.Kind().unwrap()); -// println!("Device ID: {:?}", device.Id().unwrap()); -// println!("-----------------") -// } - -// // let device_id = "PCI\\VEN_1022&DEV_790B"; - -// // let async_get_device = I2cDevice::FromIdAsync(&HSTRING::from(device_id), &connection_settings).unwrap(); -// // let device = async_get_device.get(); - -// // println!("{:?}", device); +use std::time::Duration; + +use crate::{piix4_i2c, rgb::Rgb, timer}; + +// use windows::{*, Win32::{System::LibraryLoader::*, Foundation::HCS_E_CONNECTION_CLOSED, Security::InitializeAcl}, Devices::I2c::*, core::HSTRING}; + +use crc::{Algorithm, Crc}; + +const CRC8_ALG: Algorithm = Algorithm { + width: 8, + poly: 0x7, + init: 0x0, + refin: false, + refout: false, + xorout: 0x00, + check: 0x00, + residue: 0x00, +}; + +const BUS: i32 = 0; +const BUS_ADDRESS: i32 = 0x0B00; + +// Called "device location" in 'CorsairDominatorPlatinumController' class. +const ADDRESS_DDR_1: i32 = 0x19; +const ADDRESS_DDR_2: i32 = 0x1B; + +const CORSAIR_LED_COUNT: usize = 12; + +pub struct Controller { + bus: piix4_i2c::I2c, + ddr_address: u8, +} + +impl Controller { + pub fn new(ddr_address: u8) -> Self { + Controller { + bus: piix4_i2c::I2c::new(0x0B00), + ddr_address, + } + } + + pub fn test(&self) { + self.bus.i2c_smbus_write_quick(self.ddr_address, 0); + } + + pub fn set_color(&self, color: &Rgb) { + let mut data = [0u8; CORSAIR_LED_COUNT * 3 + 2]; + data[0] = 0xC; + + for i in 0..CORSAIR_LED_COUNT { + let offset = i * 3 + 1; + data[offset] = color.red; + data[offset + 1] = color.green; + data[offset + 2] = color.blue; + } + + let crc = Crc::::new(&CRC8_ALG); + let mut digest = crc.digest(); + digest.update(&data[0..data.len() - 1]); // '-1' to not take the last byte. + data[data.len() - 1] = digest.finalize(); + + let timer = timer::Sleep::new(); + + self.bus + .write_block_data(self.ddr_address, 0x31, &data[0..piix4_i2c::I2C_BLOCK_MAX]); + timer.wait(Duration::from_micros(800)); + + self.bus + .write_block_data(self.ddr_address, 0x32, &data[piix4_i2c::I2C_BLOCK_MAX..]); + timer.wait(Duration::from_micros(200)); + } +} + +// TESTS WITH I2C from winapi: + +// let connection_settings = I2cConnectionSettings::Create(ADDRESS_DDR_1).unwrap(); + +// // For A770: "DISPLAY\\INTC_I2C\\7&3255D98A&0&UID26040" +// // "PCI\\VEN_1022&DEV_790B&SUBSYS_88771043&REV_71\\3&11583659&0&A0" + +// let selector = I2cDevice::GetDeviceSelector().unwrap(); +// println!("{:?}", selector); + +// let devices_async = Devices::Enumeration::DeviceInformation::FindAllAsync().unwrap(); // Devices::Enumeration::DeviceInformation::FindAllAsyncAqsFilter(&selector).unwrap(); +// let devices = devices_async.get().unwrap(); + +// // println!("{:?}", devices.Size()); + +// for i in 0..devices.Size().unwrap() { +// let device = devices.GetAt(i).unwrap(); +// println!("Device Name: {:?}", device.Name().unwrap()); +// println!("Device Kind: {:?}", device.Kind().unwrap()); +// println!("Device ID: {:?}", device.Id().unwrap()); +// println!("-----------------") +// } + +// // let device_id = "PCI\\VEN_1022&DEV_790B"; + +// // let async_get_device = I2cDevice::FromIdAsync(&HSTRING::from(device_id), &connection_settings).unwrap(); +// // let device = async_get_device.get(); + +// // println!("{:?}", device); diff --git a/src/gigabyte_rgb_fusion_usb.rs b/src/gigabyte_rgb_fusion_usb.rs index c9667be..0ecd9a7 100644 --- a/src/gigabyte_rgb_fusion_usb.rs +++ b/src/gigabyte_rgb_fusion_usb.rs @@ -1,6 +1,6 @@ use std::{str, time::Duration}; -use crate::rgb::RGB; +use crate::rgb::Rgb; const VID: u16 = 0x048D; // Vendor ID: Gigabyte. const PID: u16 = 0x5711; // Product ID. @@ -23,8 +23,7 @@ impl Device { .device_list() .find(|d| d.vendor_id() == VID && d.product_id() == PID && d.usage() == 204) .unwrap() - .open_device(api) - .unwrap(); + .open_device(api)?; let device = Device { device: d }; @@ -136,7 +135,7 @@ cc62390039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab const NB_LEDS_PER_PACKET: usize = 19; - fn set_color_device(&self, color: &RGB, device: u8, nb_leds: usize) -> anyhow::Result<()> { + fn set_color_device(&self, color: &Rgb, device: u8, nb_leds: usize) -> anyhow::Result<()> { let nb_packets = (nb_leds - 1) / Self::NB_LEDS_PER_PACKET + 1; for i in 0..nb_packets { let mut buffer = [0u8; 64]; @@ -165,7 +164,7 @@ cc62390039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab Ok(()) } - fn set_color_motherboard(&self, color: &RGB) -> anyhow::Result<()> { + fn set_color_motherboard(&self, color: &Rgb) -> anyhow::Result<()> { { let mut buffer = [0u8; 64]; buffer[0] = 0xCC; @@ -194,13 +193,15 @@ cc62390039ab4543 ab4543ab4543ab45 43ab4543ab4543ab4543ab4543ab4543ab4543ab4543ab Ok(()) } - pub fn set_color(&self, color: &RGB) { + pub fn set_color(&self, color: &Rgb) -> anyhow::Result<()> { // Motherboard & GPU power cables. - self.set_color_device(color, 0x58, 19).unwrap(); + self.set_color_device(color, 0x58, 19)?; // Arctic freezer 3. - self.set_color_device(color, 0x62, 38).unwrap(); + self.set_color_device(color, 0x62, 48)?; - self.set_color_motherboard(color).unwrap(); + self.set_color_motherboard(color)?; + + Ok(()) } } diff --git a/src/lian_li_sl_infinity.rs b/src/lian_li_sl_infinity.rs index cd3e149..af0e640 100644 --- a/src/lian_li_sl_infinity.rs +++ b/src/lian_li_sl_infinity.rs @@ -1,85 +1,91 @@ -use crate::rgb::RGB; - -const LIANLI_VID: u16 = 0x0CF2; -const LIANLI_UNI_HUB_SLINF_PID: u16 = 0xA102; - -const UNIHUB_SLINF_LED_MODE_STATIC_COLOR: u8 = 0x01; -const UNIHUB_SLINF_LED_SPEED_000: u8 = 0x02; -const UNIHUB_SLINF_LED_DIRECTION_LTR: u8 = 0x00; -const UNIHUB_SLINF_LED_BRIGHTNESS_100: u8 = 0x00; - -const UNIHUB_SLINF_TRANSACTION_ID: u8 = 0xE0; - -const BUFFER_SIZE: usize = 353; - -const NB_LEDS_PER_FAN: u8 = 8; -const NB_LEDS_PER_SIDE: u8 = 12; - -// Specific hardcoded values (should be given in the constructor). -const CHANNEL_COUNT: u8 = 4; // 2 Channel per line of fans: one for fan itself and one for sides. -const NB_FAN_PER_CHANNEL: u8 = 2; // 2 fans per channel. - -pub struct Device { - device: hidapi::HidDevice, -} - -impl Device { - pub fn new(api: &hidapi::HidApi) -> Self { - Self { - device: api.open(LIANLI_VID, LIANLI_UNI_HUB_SLINF_PID).unwrap(), - } - } - - fn send_start_action(&self, channel_id: u8) { - let mut buffer = [0u8; 5]; - buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; - buffer[0x01] = 0x10; - buffer[0x02] = 0x60; - buffer[0x03] = 1 + channel_id / 2; - buffer[0x04] = NB_FAN_PER_CHANNEL; - - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, BUFFER_SIZE); - } - - fn send_commit_data(&self, channel_id: u8) { - let mut buffer = [0u8; 6]; - buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; - buffer[0x01] = 0x10 + channel_id; - buffer[0x02] = UNIHUB_SLINF_LED_MODE_STATIC_COLOR; - buffer[0x03] = UNIHUB_SLINF_LED_SPEED_000; - buffer[0x04] = UNIHUB_SLINF_LED_DIRECTION_LTR; - buffer[0x05] = UNIHUB_SLINF_LED_BRIGHTNESS_100; - - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, BUFFER_SIZE); - } - - pub fn set_color(&self, color: &RGB) { - for channel_id in 0..CHANNEL_COUNT { - self.send_start_action(channel_id); - - let mut buffer = [0u8; BUFFER_SIZE]; - buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; - buffer[0x01] = 0x30 + channel_id; - - let nb_leds = if channel_id % 2 == 0 { - NB_LEDS_PER_FAN * NB_FAN_PER_CHANNEL - } else { - NB_LEDS_PER_SIDE * NB_FAN_PER_CHANNEL - }; - - for i in 0..(26 as usize) { - let pos = i * 3 + 2; - buffer[pos] = color.red; - buffer[pos + 1] = color.blue; - buffer[pos + 2] = color.green; - } - - let n_write = self.device.write(&buffer).unwrap(); - assert_eq!(n_write, buffer.len()); - - self.send_commit_data(channel_id); - } - } -} +use crate::rgb::Rgb; + +const LIANLI_VID: u16 = 0x0CF2; +const LIANLI_UNI_HUB_SLINF_PID: u16 = 0xA102; + +const UNIHUB_SLINF_LED_MODE_STATIC_COLOR: u8 = 0x01; +const UNIHUB_SLINF_LED_SPEED_000: u8 = 0x02; +const UNIHUB_SLINF_LED_DIRECTION_LTR: u8 = 0x00; +const UNIHUB_SLINF_LED_BRIGHTNESS_100: u8 = 0x00; + +const UNIHUB_SLINF_TRANSACTION_ID: u8 = 0xE0; + +const BUFFER_SIZE: usize = 353; + +const NB_LEDS_PER_FAN: u8 = 8; +const NB_LEDS_PER_SIDE: u8 = 12; + +// Specific hardcoded values (should be given in the constructor). +const CHANNEL_COUNT: u8 = 4; // 2 Channel per line of fans: one for fan itself and one for sides. +const NB_FAN_PER_CHANNEL: u8 = 2; // 2 fans per channel. + +pub struct Device { + device: hidapi::HidDevice, +} + +impl Device { + pub fn new(api: &hidapi::HidApi) -> anyhow::Result { + Ok(Self { + device: api.open(LIANLI_VID, LIANLI_UNI_HUB_SLINF_PID)?, + }) + } + + fn send_start_action(&self, channel_id: u8) -> anyhow::Result<()> { + let mut buffer = [0u8; 5]; + buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; + buffer[0x01] = 0x10; + buffer[0x02] = 0x60; + buffer[0x03] = 1 + channel_id / 2; + buffer[0x04] = NB_FAN_PER_CHANNEL; + + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, BUFFER_SIZE); + + Ok(()) + } + + fn send_commit_data(&self, channel_id: u8) -> anyhow::Result<()> { + let mut buffer = [0u8; 6]; + buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; + buffer[0x01] = 0x10 + channel_id; + buffer[0x02] = UNIHUB_SLINF_LED_MODE_STATIC_COLOR; + buffer[0x03] = UNIHUB_SLINF_LED_SPEED_000; + buffer[0x04] = UNIHUB_SLINF_LED_DIRECTION_LTR; + buffer[0x05] = UNIHUB_SLINF_LED_BRIGHTNESS_100; + + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, BUFFER_SIZE); + + Ok(()) + } + + pub fn set_color(&self, color: &Rgb) -> anyhow::Result<()> { + for channel_id in 0..CHANNEL_COUNT { + self.send_start_action(channel_id)?; + + let mut buffer = [0u8; BUFFER_SIZE]; + buffer[0x00] = UNIHUB_SLINF_TRANSACTION_ID; + buffer[0x01] = 0x30 + channel_id; + + let nb_leds = if channel_id % 2 == 0 { + NB_LEDS_PER_FAN * NB_FAN_PER_CHANNEL + } else { + NB_LEDS_PER_SIDE * NB_FAN_PER_CHANNEL + }; + + for i in 0..26 { + let pos = i * 3 + 2; + buffer[pos] = color.red; + buffer[pos + 1] = color.blue; + buffer[pos + 2] = color.green; + } + + let n_write = self.device.write(&buffer)?; + // assert_eq!(n_write, buffer.len()); + + self.send_commit_data(channel_id)?; + } + + Ok(()) + } +} diff --git a/src/machine/jiji.rs b/src/machine/jiji.rs index 402d9a4..d4e7841 100644 --- a/src/machine/jiji.rs +++ b/src/machine/jiji.rs @@ -1,57 +1,60 @@ -use crate::{asus_aura_usb, corsair_vengeance, cpu_temperature, rgb}; - -use super::Machine; - -pub struct MachineJiji { - ram: Vec, - b650e_device: asus_aura_usb::Device, - // a770: a770::A770, - // gpu_devices: intel_arc::Devices, - gpus: Vec, -} - -impl MachineJiji { - pub fn new() -> anyhow::Result { - let api = hidapi::HidApi::new().unwrap(); - Ok(MachineJiji { - ram: vec![ - corsair_vengeance::Controller::new(0x19), - corsair_vengeance::Controller::new(0x1B), - ], - b650e_device: asus_aura_usb::Device::new(&api, asus_aura_usb::Motherboard::Asus650e)?, - // a770: a770::A770::new()?, - // gpu_devices: unsafe { intel_arc::GetDevices() }, - gpus: nvapi::PhysicalGpu::enumerate()?, - }) - } -} - -impl Machine for MachineJiji { - fn set_color_1(&mut self, color: &rgb::RGB) { - for controller in &self.ram { - controller.set_color(color); - } - self.b650e_device.set_color(color).unwrap(); - } - - fn set_color_2(&mut self, color: &rgb::RGB) {} // No color 2. - - fn get_gpu_tmp(&self) -> f32 { - // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } - self.gpus[0].thermal_settings(None).unwrap()[0] - .current_temperature - .0 as f32 - } - - fn get_cpu_tmp(&self) -> f32 { - cpu_temperature::read() - } -} - -// impl Drop for MachineJiji { -// fn drop(&mut self) { -// unsafe { -// intel_arc::FreeDevices(self.gpu_devices); -// } -// } -// } +use crate::{asus_aura_usb, corsair_vengeance, cpu_temperature, rgb}; + +use super::Machine; + +pub struct MachineJiji { + ram: Vec, + b650e_device: asus_aura_usb::Device, + // a770: a770::A770, + // gpu_devices: intel_arc::Devices, + gpus: Vec, +} + +impl MachineJiji { + pub fn new() -> anyhow::Result { + let api = hidapi::HidApi::new().unwrap(); + Ok(MachineJiji { + ram: vec![ + corsair_vengeance::Controller::new(0x19), + corsair_vengeance::Controller::new(0x1B), + ], + b650e_device: asus_aura_usb::Device::new(&api, asus_aura_usb::Motherboard::Asus650e)?, + // a770: a770::A770::new()?, + // gpu_devices: unsafe { intel_arc::GetDevices() }, + gpus: nvapi::PhysicalGpu::enumerate()?, + }) + } +} + +impl Machine for MachineJiji { + fn set_color_1(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + for controller in &self.ram { + controller.set_color(color); + } + self.b650e_device.set_color(color)?; + Ok(()) + } + + fn set_color_2(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + Ok(()) + } // No color 2. + + fn get_gpu_tmp(&self) -> f32 { + // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } + self.gpus[0].thermal_settings(None).unwrap()[0] + .current_temperature + .0 as f32 + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} + +// impl Drop for MachineJiji { +// fn drop(&mut self) { +// unsafe { +// intel_arc::FreeDevices(self.gpu_devices); +// } +// } +// } diff --git a/src/machine/lyss_metal.rs b/src/machine/lyss_metal.rs index ba4cc43..adf8205 100644 --- a/src/machine/lyss_metal.rs +++ b/src/machine/lyss_metal.rs @@ -1,115 +1,117 @@ -use nvapi::sys::i2c; - -use crate::{asus_aura_usb, corsair_lighting_pro, cpu_temperature, lian_li_sl_infinity, rgb}; - -use super::Machine; - -pub struct MachineLyssMetal { - crosshair_device: asus_aura_usb::Device, - corsair_lignting_pro: corsair_lighting_pro::Device, - lian_li_sl_infinity: lian_li_sl_infinity::Device, - gpus: Vec, -} - -impl MachineLyssMetal { - pub fn new() -> anyhow::Result { - let api = hidapi::HidApi::new()?; - - nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); - - let machine = Self { - crosshair_device: asus_aura_usb::Device::new( - &api, - asus_aura_usb::Motherboard::AsusCrosshairVIIIHero, - )?, - corsair_lignting_pro: corsair_lighting_pro::Device::new( - &api, - &rgb::RGB { - red: 0, - green: 255, - blue: 40, - }, - ), - lian_li_sl_infinity: lian_li_sl_infinity::Device::new(&api), - gpus: nvapi::PhysicalGpu::enumerate()?, - }; - - // machine.set_mode_3080ti(); - Ok(machine) - } - - // Doesn't work: "Error: NotSupported". - // From OpenRGB, see the following files: - // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUControllerDetect.cpp - // * Controllers\GigabyteRGBFusion2GPUController\RGBController_GigabyteRGBFusion2GPU.cpp - // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp - // * i2c_smbus\i2c_smbus_nvapi.cpp - // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 - // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html - pub fn test_i2c(&self) { - // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' - let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; - self.gpus[0] - .i2c_write( - 0, - Some(1), - false, - super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, - &[], - &data, - i2c::I2cSpeed::Default, - ) - .expect("Error"); - } - - fn set_mode_3080ti(&self) { - let data = [ - super::RGB_FUSION2_GPU_REG_MODE, - 0x01, // Mode (1: static). - 0x00, // Speed. - 0x63, // Brightness max. - 0x00, // Mistery flag. - 0x01, // Zone. - 0x00, - 0x00, - ]; - self.gpus[0] - .i2c_write( - 0, - Some(1), - false, - super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, - &[], - &data, - i2c::I2cSpeed::Default, - ) - .expect("Error"); - } - - fn set_color_3080ti(&self, color: &rgb::RGB) { - // TODO. - self.test_i2c(); - } -} - -impl Machine for MachineLyssMetal { - fn set_color_1(&mut self, color: &rgb::RGB) { - self.crosshair_device.set_color(color).unwrap(); - self.corsair_lignting_pro.set_color(color); - // self.set_color_3080ti(&color); // TODO. - } - - fn set_color_2(&mut self, color: &rgb::RGB) { - self.lian_li_sl_infinity.set_color(color); - } - - fn get_gpu_tmp(&self) -> f32 { - self.gpus[0].thermal_settings(None).unwrap()[0] - .current_temperature - .0 as f32 - } - - fn get_cpu_tmp(&self) -> f32 { - cpu_temperature::read() - } -} +use nvapi::sys::i2c; + +use crate::{asus_aura_usb, corsair_lighting_pro, cpu_temperature, lian_li_sl_infinity, rgb}; + +use super::Machine; + +pub struct MachineLyssMetal { + crosshair_device: asus_aura_usb::Device, + corsair_lignting_pro: corsair_lighting_pro::Device, + lian_li_sl_infinity: lian_li_sl_infinity::Device, + gpus: Vec, +} + +impl MachineLyssMetal { + pub fn new() -> anyhow::Result { + let api = hidapi::HidApi::new()?; + + nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); + + let machine = Self { + crosshair_device: asus_aura_usb::Device::new( + &api, + asus_aura_usb::Motherboard::AsusCrosshairVIIIHero, + )?, + corsair_lignting_pro: corsair_lighting_pro::Device::new( + &api, + &rgb::Rgb { + red: 0, + green: 255, + blue: 40, + }, + )?, + lian_li_sl_infinity: lian_li_sl_infinity::Device::new(&api)?, + gpus: nvapi::PhysicalGpu::enumerate()?, + }; + + // machine.set_mode_3080ti(); + Ok(machine) + } + + // Doesn't work: "Error: NotSupported". + // From OpenRGB, see the following files: + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUControllerDetect.cpp + // * Controllers\GigabyteRGBFusion2GPUController\RGBController_GigabyteRGBFusion2GPU.cpp + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp + // * i2c_smbus\i2c_smbus_nvapi.cpp + // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 + // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html + pub fn test_i2c(&self) { + // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' + let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_mode_3080ti(&self) { + let data = [ + super::RGB_FUSION2_GPU_REG_MODE, + 0x01, // Mode (1: static). + 0x00, // Speed. + 0x63, // Brightness max. + 0x00, // Mistery flag. + 0x01, // Zone. + 0x00, + 0x00, + ]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_color_3080ti(&self, color: &rgb::Rgb) { + // TODO. + self.test_i2c(); + } +} + +impl Machine for MachineLyssMetal { + fn set_color_1(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + self.crosshair_device.set_color(color)?; + self.corsair_lignting_pro.set_color(color)?; + // self.set_color_3080ti(&color); // TODO. + Ok(()) + } + + fn set_color_2(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + self.lian_li_sl_infinity.set_color(color)?; + Ok(()) + } + + fn get_gpu_tmp(&self) -> f32 { + self.gpus[0].thermal_settings(None).unwrap()[0] + .current_temperature + .0 as f32 + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} diff --git a/src/machine/lyss_metal2.rs b/src/machine/lyss_metal2.rs index f96d213..f5ec827 100644 --- a/src/machine/lyss_metal2.rs +++ b/src/machine/lyss_metal2.rs @@ -1,114 +1,117 @@ -use nvapi::sys::i2c; - -use crate::{ - corsair_lighting_pro, cpu_temperature, gigabyte_rgb_fusion_usb, lian_li_sl_infinity, rgb, -}; - -use super::Machine; - -pub struct MachineLyssMetal2 { - fusion_device: gigabyte_rgb_fusion_usb::Device, - corsair_lignting_pro: corsair_lighting_pro::Device, - lian_li_sl_infinity: lian_li_sl_infinity::Device, - gpus: Vec, -} - -impl MachineLyssMetal2 { - pub fn new() -> anyhow::Result { - let api = hidapi::HidApi::new()?; - - nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); - - let machine = Self { - fusion_device: gigabyte_rgb_fusion_usb::Device::new(&api)?, - corsair_lignting_pro: corsair_lighting_pro::Device::new( - &api, - &rgb::RGB { - red: 0, - green: 255, - blue: 40, - }, - ), - lian_li_sl_infinity: lian_li_sl_infinity::Device::new(&api), - gpus: nvapi::PhysicalGpu::enumerate()?, - }; - - // machine.set_mode_3080ti(); - Ok(machine) - } - - // Doesn't work: "Error: NotSupported". - // From OpenRGB, see the following files: - // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUControllerDetect.cpp - // * Controllers\GigabyteRGBFusion2GPUController\RGBController_GigabyteRGBFusion2GPU.cpp - // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp - // * i2c_smbus\i2c_smbus_nvapi.cpp - // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 - // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html - pub fn test_i2c(&self) { - // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' - let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; - self.gpus[0] - .i2c_write( - 0, - Some(1), - false, - super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, - &[], - &data, - i2c::I2cSpeed::Default, - ) - .expect("Error"); - } - - fn set_mode_3080ti(&self) { - let data = [ - super::RGB_FUSION2_GPU_REG_MODE, - 0x01, // Mode (1: static). - 0x00, // Speed. - 0x63, // Brightness max. - 0x00, // Mistery flag. - 0x01, // Zone. - 0x00, - 0x00, - ]; - self.gpus[0] - .i2c_write( - 0, - Some(1), - false, - super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, - &[], - &data, - i2c::I2cSpeed::Default, - ) - .expect("Error"); - } - - fn set_color_3080ti(&self, color: &rgb::RGB) { - // TODO. - self.test_i2c(); - } -} - -impl Machine for MachineLyssMetal2 { - fn set_color_1(&mut self, color: &rgb::RGB) { - self.corsair_lignting_pro.set_color(color); - self.fusion_device.set_color(color); - // self.set_color_3080ti(&color); // TODO. - } - - fn set_color_2(&mut self, color: &rgb::RGB) { - self.lian_li_sl_infinity.set_color(color); - } - - fn get_gpu_tmp(&self) -> f32 { - self.gpus[0].thermal_settings(None).unwrap()[0] - .current_temperature - .0 as f32 - } - - fn get_cpu_tmp(&self) -> f32 { - cpu_temperature::read() - } -} +use nvapi::sys::i2c; + +use crate::{ + corsair_lighting_pro, cpu_temperature, gigabyte_rgb_fusion_usb, lian_li_sl_infinity, rgb, +}; + +use super::Machine; + +pub struct MachineLyssMetal2 { + fusion_device: gigabyte_rgb_fusion_usb::Device, + corsair_lignting_pro: corsair_lighting_pro::Device, + lian_li_sl_infinity: lian_li_sl_infinity::Device, + gpus: Vec, +} + +impl MachineLyssMetal2 { + pub fn new() -> anyhow::Result { + let api = hidapi::HidApi::new()?; + + nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); + + let machine = Self { + fusion_device: gigabyte_rgb_fusion_usb::Device::new(&api)?, + corsair_lignting_pro: corsair_lighting_pro::Device::new( + &api, + &rgb::Rgb { + red: 0, + green: 255, + blue: 40, + }, + )?, + lian_li_sl_infinity: lian_li_sl_infinity::Device::new(&api)?, + gpus: nvapi::PhysicalGpu::enumerate()?, + }; + + // machine.set_mode_3080ti(); + Ok(machine) + } + + // Doesn't work: "Error: NotSupported". + // From OpenRGB, see the following files: + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUControllerDetect.cpp + // * Controllers\GigabyteRGBFusion2GPUController\RGBController_GigabyteRGBFusion2GPU.cpp + // * Controllers\GigabyteRGBFusion2GPUController\GigabyteRGBFusion2GPUController.cpp + // * i2c_smbus\i2c_smbus_nvapi.cpp + // Implementation of nvapi-rs: https://github.com/arcnmx/nvapi-rs/blob/master/src/gpu.rs#L645 + // Reference API doc: https://docs.nvidia.com/gameworks/content/gameworkslibrary/coresdk/nvapi/structNV__I2C__INFO__V3.html + pub fn test_i2c(&self) { + // Test from 'GigabyteRGBFusion2GPUControllerDetect.cpp' + let data = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_mode_3080ti(&self) { + let data = [ + super::RGB_FUSION2_GPU_REG_MODE, + 0x01, // Mode (1: static). + 0x00, // Speed. + 0x63, // Brightness max. + 0x00, // Mistery flag. + 0x01, // Zone. + 0x00, + 0x00, + ]; + self.gpus[0] + .i2c_write( + 0, + Some(1), + false, + super::GIGABYTE_RTX3080TI_VISION_OC_ADDR, + &[], + &data, + i2c::I2cSpeed::Default, + ) + .expect("Error"); + } + + fn set_color_3080ti(&self, color: &rgb::Rgb) { + // TODO. + self.test_i2c(); + } +} + +impl Machine for MachineLyssMetal2 { + fn set_color_1(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + self.corsair_lignting_pro.set_color(color)?; + self.fusion_device.set_color(color)?; + Ok(()) + // self.set_color_3080ti(&color); // TODO. + } + + fn set_color_2(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + self.lian_li_sl_infinity.set_color(color)?; + Ok(()) + } + + fn get_gpu_tmp(&self) -> f32 { + match self.gpus[0].thermal_settings(None) { + Ok(thermal) => thermal[0].current_temperature.0 as f32, + Err(_) => 0., + } + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} diff --git a/src/machine/mod.rs b/src/machine/mod.rs index 0ed2ff5..f6cddfe 100644 --- a/src/machine/mod.rs +++ b/src/machine/mod.rs @@ -1,21 +1,22 @@ -use crate::rgb; - -pub mod jiji; -pub mod lyss_metal; -pub mod lyss_metal2; - -const RGB_FUSION2_GPU_REG_COLOR: u8 = 0x40; -const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; - -const GIGABYTE_RTX3080TI_VISION_OC_ADDR: u8 = 0x63; - -pub trait Machine { - fn set_color(&mut self, color: &rgb::RGB) { - self.set_color_1(color); - self.set_color_2(color); - } - fn set_color_1(&mut self, color: &rgb::RGB); - fn set_color_2(&mut self, color: &rgb::RGB); - fn get_gpu_tmp(&self) -> f32; - fn get_cpu_tmp(&self) -> f32; -} +use crate::rgb; + +pub mod jiji; +pub mod lyss_metal; +pub mod lyss_metal2; + +const RGB_FUSION2_GPU_REG_COLOR: u8 = 0x40; +const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; + +const GIGABYTE_RTX3080TI_VISION_OC_ADDR: u8 = 0x63; + +pub trait Machine { + fn set_color(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + self.set_color_1(color)?; + self.set_color_2(color)?; + Ok(()) + } + fn set_color_1(&mut self, color: &rgb::Rgb) -> anyhow::Result<()>; + fn set_color_2(&mut self, color: &rgb::Rgb) -> anyhow::Result<()>; + fn get_gpu_tmp(&self) -> f32; + fn get_cpu_tmp(&self) -> f32; +} diff --git a/src/main.rs b/src/main.rs index ff87264..ed4eccb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,242 +1,321 @@ -#[macro_use] -extern crate windows_service; - -use std::{ - env, - ffi::OsString, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - thread::sleep, - time::{self, Duration}, -}; - -use anyhow::Result; -use log::{debug, error, info, trace, warn}; -use windows::Win32::Foundation::{ERROR_SERVICE_DOES_NOT_EXIST, WIN32_ERROR}; -use windows_service::{ - service::{ - ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, - ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, - }, - service_control_handler::{self, ServiceControlHandlerResult, ServiceStatusHandle}, - service_dispatcher, - service_manager::{ServiceManager, ServiceManagerAccess}, -}; - -define_windows_service!(ffi_service_main, service_main); - -mod wrapper_winring0 { - #![allow(warnings, unused)] - include!(concat!(env!("OUT_DIR"), "/ols_api.rs")); -} -mod intel_arc { - #![allow(warnings, unused)] - include!(concat!(env!("OUT_DIR"), "/intel_arc.rs")); -} -mod a770; -mod asus_aura_usb; -mod corsair_lighting_pro; -mod gigabyte_rgb_fusion_usb; -mod lian_li_sl_infinity; -mod machine; -mod main_loop; -mod winring0; -// mod common; -mod consts; -mod corsair_vengeance; -mod piix4_i2c; -mod rgb; -// mod roccat; Disabled. -mod cpu_temperature; -mod settings; -mod tests; -mod timer; - -// Important: when starting as a service, the directory where the log and config files -// are put is 'C:\Windows\System32\config\systemprofile\AppData\Roaming\Temp2RGB'. -fn main() -> Result<()> { - let is_debug = cfg!(debug_assertions); - - flexi_logger::Logger::try_with_str(if is_debug { "debug" } else { "info" })? - .log_to_file( - flexi_logger::FileSpec::default() - .directory(dirs::config_dir().unwrap().join(consts::SERVICE_NAME)) - .basename(consts::SERVICE_NAME), - ) - .duplicate_to_stdout(flexi_logger::Duplicate::All) - .format(if is_debug { - flexi_logger::default_format - } else { - flexi_logger::detailed_format - }) - .rotate( - flexi_logger::Criterion::Size(1024 * 1024), - flexi_logger::Naming::Timestamps, - flexi_logger::Cleanup::KeepLogFiles(10), - ) - .print_message() - .start()?; - - log_panics::init(); - - let args: Vec = env::args().collect(); - - info!("Temperature to RGB"); - - if args.contains(&"--no-service".to_string()) { - let completed: Arc = Arc::new(AtomicBool::new(false)); - main_loop::main_loop(completed.clone()); - } else if args.contains(&"--tests".to_string()) { - tests::tests(); - } else if args.contains(&"--install-service".to_string()) { - println!("Installing service..."); - install_service()?; - } else if args.contains(&"--uninstall-service".to_string()) { - println!("Uninstalling service..."); - uninstall_service()?; - } else { - service_dispatcher::start(consts::SERVICE_NAME, ffi_service_main)?; - } - - Ok(()) -} - -fn install_service() -> windows_service::Result<()> { - let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - - let service_binary_path = std::env::current_exe() - .unwrap() - .with_file_name("temp_2_rgb.exe"); - - println!("Installing service: {service_binary_path:?}"); - - let service_info = ServiceInfo { - name: OsString::from(consts::SERVICE_NAME), - display_name: OsString::from(consts::SERVICE_NAME), - service_type: ServiceType::OWN_PROCESS, - start_type: ServiceStartType::AutoStart, - error_control: ServiceErrorControl::Normal, - executable_path: service_binary_path, - launch_arguments: vec![], - dependencies: vec![], - account_name: None, // run as System - account_password: None, - }; - let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; - service.set_description( - "A service to set the color of hardware according to the temperature of GPU and CPU", - )?; - Ok(()) -} - -fn uninstall_service() -> windows_service::Result<()> { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - - let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; - let service = service_manager.open_service(consts::SERVICE_NAME, service_access)?; - - // The service will be marked for deletion as long as this function call succeeds. - // However, it will not be deleted from the database until it is stopped and all open handles to it are closed. - service.delete()?; - - // Our handle to it is not closed yet. So we can still query it. - if service.query_status()?.current_state != ServiceState::Stopped { - // If the service cannot be stopped, it will be deleted when the system restarts. - service.stop()?; - } - - // Explicitly close our open handle to the service. This is automatically called when `service` goes out of scope. - drop(service); - - // Win32 API does not give us a way to wait for service deletion. - // To check if the service is deleted from the database, we have to poll it ourselves. - let start = time::Instant::now(); - let timeout = Duration::from_secs(5); - while start.elapsed() < timeout { - if let Err(windows_service::Error::Winapi(e)) = - service_manager.open_service(consts::SERVICE_NAME, ServiceAccess::QUERY_STATUS) - { - let WIN32_ERROR(error_num) = ERROR_SERVICE_DOES_NOT_EXIST; - if e.raw_os_error() == Some(error_num as i32) { - println!("{} is deleted.", consts::SERVICE_NAME); - return Ok(()); - } - } - sleep(Duration::from_secs(1)); - } - println!("{} is marked for deletion.", consts::SERVICE_NAME); - - Ok(()) -} - -fn service_main(arguments: Vec) { - if let Err(error) = run_service(arguments) { - error!("{error}"); - } -} - -fn run_service(_arguments: Vec) -> Result<(), windows_service::Error> { - let completed: Arc = Arc::new(AtomicBool::new(false)); - - let completed_event_handler = Arc::clone(&completed); - - info!("Setup the event handler..."); - - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - ServiceControl::Stop => { - completed_event_handler.store(true, Ordering::Relaxed); - // Handle stop event and return control back to the system. - ServiceControlHandlerResult::NoError - } - ServiceControl::Shutdown => { - completed_event_handler.store(true, Ordering::Relaxed); - // Handle stop event and return control back to the system. - ServiceControlHandlerResult::NoError - } - // ServiceControl::Preshutdown => { - // completed_event_handler.store(true, Ordering::Relaxed); - // ServiceControlHandlerResult::NoError - // } - // ServiceControl::PowerEvent(param) => { - // ServiceControlHandlerResult::NotImplemented - // } - // All services must accept Interrogate even if it's a no-op. - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - - // Register system service event handler - let status_handle = service_control_handler::register(consts::SERVICE_NAME, event_handler)?; - - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, //Some(std::process::id()), - })?; - - main_loop::main_loop(completed.clone()); - - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, //Some(std::process::id()), - })?; - - info!("Main loop stopped: Temperature to RGB will now shut down"); - - Ok(()) -} +#[macro_use] +extern crate windows_service; + +use std::{ + ffi::OsString, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::sleep, + time::{self, Duration}, +}; + +use anyhow::Result; +use clap::Parser; +use log::{error, info}; +use windows::Win32::Foundation::{ERROR_SERVICE_DOES_NOT_EXIST, WIN32_ERROR}; +use windows_service::{ + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, + ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; + +define_windows_service!(ffi_service_main, service_main); + +mod wrapper_winring0 { + #![allow(warnings, unused)] + include!(concat!(env!("OUT_DIR"), "/ols_api.rs")); +} +// mod intel_arc { +// #![allow(warnings, unused)] +// include!(concat!(env!("OUT_DIR"), "/intel_arc.rs")); +// } +mod a770; +mod asus_aura_usb; +mod consts; +mod corsair_lighting_pro; +mod corsair_vengeance; +mod gigabyte_rgb_fusion_usb; +mod lian_li_sl_infinity; +mod machine; +mod main_loop; +mod piix4_i2c; +mod rgb; +mod winring0; +// mod roccat; Disabled. +mod cpu_temperature; +mod settings; +mod tests; +mod timer; + +#[derive(Parser, Debug)] +#[command( + author = "Greg Burri", + version = "1.0", + about = "Set RGB according to CPU and GPU temperaturess" +)] +struct Args { + /// Launch without service. + #[arg(group = "main", long)] + no_service: bool, + + /// Run tests. + #[arg(group = "main", long)] + tests: bool, + + /// Install driver winring0. + #[arg(group = "main", long)] + install_winring0: bool, + + /// Install the service. + #[arg(group = "main", long)] + install_service: bool, + + /// Uninstall the service. + #[arg(group = "main", long)] + uninstall_service: bool, +} + +// Important: when starting as a service, the directory where the log and config files +// are put is 'C:\Windows\System32\config\systemprofile\AppData\Roaming\Temp2RGB'. +fn main() -> Result<()> { + let is_debug = cfg!(debug_assertions); + + flexi_logger::Logger::try_with_str(if is_debug { "debug" } else { "info" })? + .log_to_file( + flexi_logger::FileSpec::default() + .directory(dirs::config_dir().unwrap().join(consts::SERVICE_NAME)) + .basename(consts::SERVICE_NAME), + ) + .duplicate_to_stdout(flexi_logger::Duplicate::All) + .format(if is_debug { + flexi_logger::default_format + } else { + flexi_logger::detailed_format + }) + .rotate( + flexi_logger::Criterion::Size(1024 * 1024), + flexi_logger::Naming::Timestamps, + flexi_logger::Cleanup::KeepLogFiles(10), + ) + .print_message() + .start()?; + + log_panics::init(); + + let args = Args::parse(); + + info!("Temperature to RGB"); + + if args.no_service { + let completed: Arc = Arc::new(AtomicBool::new(false)); + main_loop::main_loop(completed.clone()); + } else if args.tests { + tests::tests(); + } else if args.install_winring0 { + println!("Installing winring0 service..."); + install_winring0()?; + } else if args.install_service { + println!("Installing service..."); + install_service()?; + } else if args.uninstall_service { + println!("Uninstalling service..."); + uninstall_service()?; + } else { + service_dispatcher::start(consts::SERVICE_NAME, ffi_service_main)?; + } + + Ok(()) +} + +fn install_winring0() -> windows_service::Result<()> { + let system_dir = unsafe { + let mut system_dir = [0u8; windows::Win32::Foundation::MAX_PATH as usize]; + let l = + windows::Win32::System::SystemInformation::GetSystemDirectoryA(Some(&mut system_dir)) + as usize; + assert_ne!(l, 0); + String::from_utf8(system_dir[0..l].into()).unwrap() + }; + // TODO: to const. + let winring0_filename = "WinRing0x64.sys"; + let driver_name = "WinRing0x64"; + + let winring0_path = std::env::current_exe() + .unwrap() + .with_file_name(winring0_filename); + + let destination = std::path::Path::new(&system_dir) + .join("drivers") + .join(winring0_filename); + + std::fs::copy(winring0_path, &destination).unwrap(); + + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; + + // println!("Installing service: {service_binary_path:?}"); + + let service_info = ServiceInfo { + name: OsString::from(driver_name), + display_name: OsString::from(driver_name), + service_type: ServiceType::KERNEL_DRIVER, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: destination, + launch_arguments: vec![], + dependencies: vec![], + account_name: None, // run as System + account_password: None, + }; + + let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; + service.set_description("Winring0")?; + + Ok(()) +} + +fn install_service() -> windows_service::Result<()> { + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; + + let service_binary_path = std::env::current_exe() + .unwrap() + .with_file_name("temp_2_rgb.exe"); + + println!("Installing service: {service_binary_path:?}"); + + let service_info = ServiceInfo { + name: OsString::from(consts::SERVICE_NAME), + display_name: OsString::from(consts::SERVICE_NAME), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: service_binary_path, + launch_arguments: vec![], + dependencies: vec![], + account_name: None, // run as System + account_password: None, + }; + + let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; + service.set_description( + "A service to set the color of hardware according to the temperature of GPU and CPU", + )?; + + Ok(()) +} + +fn uninstall_service() -> windows_service::Result<()> { + let manager_access = ServiceManagerAccess::CONNECT; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; + + let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; + let service = service_manager.open_service(consts::SERVICE_NAME, service_access)?; + + // The service will be marked for deletion as long as this function call succeeds. + // However, it will not be deleted from the database until it is stopped and all open handles to it are closed. + service.delete()?; + + // Our handle to it is not closed yet. So we can still query it. + if service.query_status()?.current_state != ServiceState::Stopped { + // If the service cannot be stopped, it will be deleted when the system restarts. + service.stop()?; + } + + // Explicitly close our open handle to the service. This is automatically called when `service` goes out of scope. + drop(service); + + // Win32 API does not give us a way to wait for service deletion. + // To check if the service is deleted from the database, we have to poll it ourselves. + let start = time::Instant::now(); + let timeout = Duration::from_secs(5); + while start.elapsed() < timeout { + if let Err(windows_service::Error::Winapi(e)) = + service_manager.open_service(consts::SERVICE_NAME, ServiceAccess::QUERY_STATUS) + { + let WIN32_ERROR(error_num) = ERROR_SERVICE_DOES_NOT_EXIST; + if e.raw_os_error() == Some(error_num as i32) { + println!("{} is deleted.", consts::SERVICE_NAME); + return Ok(()); + } + } + sleep(Duration::from_secs(1)); + } + println!("{} is marked for deletion.", consts::SERVICE_NAME); + + Ok(()) +} + +fn service_main(arguments: Vec) { + if let Err(error) = run_service(arguments) { + error!("{error}"); + } +} + +fn run_service(_arguments: Vec) -> Result<(), windows_service::Error> { + let completed: Arc = Arc::new(AtomicBool::new(false)); + + let completed_event_handler = Arc::clone(&completed); + + info!("Setup the event handler..."); + + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop => { + completed_event_handler.store(true, Ordering::Relaxed); + // Handle stop event and return control back to the system. + ServiceControlHandlerResult::NoError + } + ServiceControl::Shutdown => { + completed_event_handler.store(true, Ordering::Relaxed); + // Handle stop event and return control back to the system. + ServiceControlHandlerResult::NoError + } + // ServiceControl::Preshutdown => { + // completed_event_handler.store(true, Ordering::Relaxed); + // ServiceControlHandlerResult::NoError + // } + // ServiceControl::PowerEvent(param) => { + // ServiceControlHandlerResult::NotImplemented + // } + // All services must accept Interrogate even if it's a no-op. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register(consts::SERVICE_NAME, event_handler)?; + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, //Some(std::process::id()), + })?; + + main_loop::main_loop(completed.clone()); + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, //Some(std::process::id()), + })?; + + info!("Main loop stopped: Temperature to RGB will now shut down"); + + Ok(()) +} diff --git a/src/main_loop.rs b/src/main_loop.rs index 1d523b3..a8b7618 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -1,11 +1,13 @@ use std::{ sync::{ - atomic::{AtomicBool, Ordering}, Arc, + atomic::{AtomicBool, Ordering}, }, time::{self, Duration}, }; +use log::warn; + use crate::{consts, machine, rgb, settings, timer, winring0}; pub fn main_loop(completed: Arc) { @@ -98,12 +100,15 @@ pub fn main_loop(completed: Arc) { if tick % (consts::FREQ_TEMP_POLLING / consts::FREQ_REFRESHING_RGB) as i64 == 0 { println!("Update RGB: {color_1:?}/{color_2:?}, temp: {mean_temp}"); - machine.set_color_1(&color_1); - if color_2.is_some() { - machine.set_color_2(&color_2.unwrap()); - } else { - machine.set_color_2(&color_1); - } + if let Err(error) = machine.set_color_1(&color_1) { + warn!("Unable to set color 1: {}", error); + }; + + if let Err(error) = + machine.set_color_2(&if let Some(c) = color_2 { c } else { color_1 }) + { + warn!("Unable to set color 2: {}", error); + }; } let elapsed = time::Instant::now() - time_beginning_loop; diff --git a/src/piix4_i2c.rs b/src/piix4_i2c.rs index 7397459..8138e06 100644 --- a/src/piix4_i2c.rs +++ b/src/piix4_i2c.rs @@ -1,274 +1,273 @@ -// Partial implementation for PCI IDE ISA Xcelerator. -// https://www.kernel.org/doc/html/latest/i2c/summary.html - -use std::time::Duration; - -use crate::{timer, wrapper_winring0}; - -pub const I2C_BLOCK_MAX: usize = 32; - -#[repr(u16)] -#[derive(Clone, Copy, Debug)] -enum TransactionType { - I2cSmbusQuick = 0, - I2cSmbusByte = 1, - I2cSmbusByteData = 2, - I2cSmbusWordData = 3, - I2cSmbusProcCall = 4, - I2cSmbusBlockData = 5, - I2cSmbusI2cBlockBroken = 6, - I2cSmbusBlockProcCall = 7, /* SMBus 2.0 */ - I2cSmbusI2cBlockData = 8, -} - -#[repr(u16)] -#[derive(Clone, Copy, Debug)] -enum Piix4TransactionType { - Piix4Quick = 0x00, - Piix4Byte = 0x04, - Piix4ByteData = 0x08, - Piix4WordData = 0x0C, - Piix4BlockData = 0x14, -} - -// PIIX4 SMBus address offsets - -#[repr(u16)] -#[derive(Clone, Copy, Debug)] -enum SMBusAddressOffsets { - Smbhststs = 0, - Smbhslvsts = 1, - Smbhstcnt = 2, - Smbhstcmd = 3, - Smbhstadd = 4, - Smbhstdat0 = 5, - Smbhstdat1 = 6, - Smbblkdat = 7, - Smbslvcnt = 8, - Smbshdwcmd = 9, - Smbslvevt = 0xA, - Smbslvdat = 0xC, -} - -#[repr(u8)] -#[derive(Clone, Copy)] -enum AccessType { - Write = 0, - Read = 1, -} - -pub struct I2c { - base_address: u16, -} - -enum XferResult { - Ok, - BlockData(Vec), -} - -#[derive(Debug)] -enum Error { - Busy, - Timeout, - IO, - Data, -} - -impl I2c { - pub fn new(base_address: u16) -> Self { - I2c { base_address } - } - - pub fn write_block_data(&self, addr: u8, command: u8, data: &[u8]) { - let l = data.len(); - assert!( - l <= I2C_BLOCK_MAX, - "Data length must not exceed {}", - I2C_BLOCK_MAX - ); - let mut data_block = [0u8; I2C_BLOCK_MAX + 2]; - data_block[0] = l as u8; - data_block[1..l + 1].copy_from_slice(data); - - unsafe { - if let Err(error) = self.i2c_smbus_xfer( - addr, - AccessType::Write, - command, - TransactionType::I2cSmbusBlockData, - Some(&data_block), - ) { - println!("Error when writing block (I2c): {error:?}"); - } - } - } - - pub fn i2c_smbus_write_quick(&self, addr: u8, value: u8) { - unsafe { - self.i2c_smbus_xfer( - addr, - AccessType::Write, - value, - TransactionType::I2cSmbusQuick, - None, - ) - .unwrap(); - } - } - - unsafe fn i2c_smbus_xfer( - &self, - addr: u8, - access_type: AccessType, - command: u8, - transaction_type: TransactionType, // Called 'size' in 'i2c_smbus\i2c_smbus_piix4.cpp'. - data: Option<&[u8]>, - ) -> Result { - let piix4_transaction_type = match transaction_type { - TransactionType::I2cSmbusQuick => { - self.write_io_port_byte( - SMBusAddressOffsets::Smbhstadd, - (addr << 1) | access_type as u8, - ); - Piix4TransactionType::Piix4Quick - } - TransactionType::I2cSmbusByte => todo!(), - TransactionType::I2cSmbusByteData => todo!(), // Here 'data' should be a byte, maybe using a enum?. - TransactionType::I2cSmbusWordData => todo!(), // Here 'data' should be a u16, maybe using a enum?. - TransactionType::I2cSmbusBlockData => { - self.write_io_port_byte( - SMBusAddressOffsets::Smbhstadd, - (addr << 1) | access_type as u8, - ); - self.write_io_port_byte(SMBusAddressOffsets::Smbhstcmd, command); - if let AccessType::Write = access_type { - let len = data.unwrap()[0]; - if len == 0 || len > I2C_BLOCK_MAX as u8 { - panic!("Invalid len value: {}", len); - } - - self.write_io_port_byte(SMBusAddressOffsets::Smbhstdat0, len); - self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); // TODO: do something of the result!? - for i in 1..=len { - self.write_io_port_byte( - SMBusAddressOffsets::Smbblkdat, - data.unwrap()[i as usize], - ); - } - } - Piix4TransactionType::Piix4BlockData - } - _ => panic!("Not supported: {:?}", transaction_type), - }; - - self.write_io_port_byte( - SMBusAddressOffsets::Smbhstcnt, - piix4_transaction_type as u8 & 0x1C, - ); - - self.piix4_transaction()?; - - // if let (AccessType::Write, Piix4TransactionType::Piix4Quick) = (access_type, piix4_transaction_type) { - // return Ok(()) - // } - - match piix4_transaction_type { - Piix4TransactionType::Piix4Quick => Ok(XferResult::Ok), - Piix4TransactionType::Piix4Byte => todo!(), - Piix4TransactionType::Piix4ByteData => todo!(), - Piix4TransactionType::Piix4WordData => todo!(), - Piix4TransactionType::Piix4BlockData => { - let l = self.read_io_port_byte(SMBusAddressOffsets::Smbhstdat0) as usize; - if l == 0 || l > I2C_BLOCK_MAX { - return Err(Error::Data); - } - self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); - let mut data = vec![0; l + 1]; - for i in 1..=l { - data[i] = self.read_io_port_byte(SMBusAddressOffsets::Smbblkdat); - } - Ok(XferResult::BlockData(data)) - } - } - } - - unsafe fn piix4_transaction(&self) -> Result<(), Error> { - let timer = timer::Sleep::new(); - - // Make sure the SMBus is ready to start transmitting. - let mut res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); - if res != 0x00 { - self.write_io_port_byte(SMBusAddressOffsets::Smbhststs, res); - res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); - if res != 0x00 { - return Err(Error::Busy); - } - } - - // Start the transaction by setting bit 6. - res = self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); - self.write_io_port_byte(SMBusAddressOffsets::Smbhstcnt, res | 0x40); - - // let duration: i64 = -2_500; // 250 us. - let mut n = 0; - loop { - timer.wait(Duration::from_micros(250)); - - res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); - // println!("Res: {}", res); - if res > 0x01 { - break; - } - - if n >= 100 { - return Err(Error::Timeout); - } - n += 1; - } - // println!("-----"); - - if res & 0x10 != 0x00 || res & 0x08 != 0x0 || res & 0x04 != 0x0 { - return Err(Error::IO); - } - - res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); - if res != 0x00 { - self.write_io_port_byte(SMBusAddressOffsets::Smbhststs, res); - } - - Ok(()) - } - - unsafe fn write_io_port_byte(&self, op: SMBusAddressOffsets, value: u8) { - wrapper_winring0::WriteIoPortByte(self.base_address + op as u16, value); - } - - unsafe fn read_io_port_byte(&self, op: SMBusAddressOffsets) -> u8 { - wrapper_winring0::ReadIoPortByte(self.base_address + op as u16) - } -} - -/* -type ADL_MAIN_MALLOC_CALLBACK = unsafe fn(c_int) -> *mut c_void; -type ADL_CONTEXT_HANDLE = *mut c_void; - -type ADL2_MAIN_CONTROL_CREATE = unsafe extern "C" fn(ADL_MAIN_MALLOC_CALLBACK, c_int, *mut ADL_CONTEXT_HANDLE) -> c_int; -type ADL2_MAIN_CONTROL_DESTROY = unsafe extern "C" fn(ADL_CONTEXT_HANDLE) -> c_int; -type ADL2_ADAPTER_NUMBEROFADAPTERS_GET = unsafe extern "C" fn(ADL_CONTEXT_HANDLE, *mut c_int) -> c_int; - -pub fn test() { - unsafe { - let hDLL = LoadLibraryW(w!("atiadlxx.dll")).unwrap(); - println!("{:?}", hDLL); - - let ADL2_Main_Control_Create: ADL2_MAIN_CONTROL_CREATE = transmute(&GetProcAddress(hDLL, s!("ADL2_Main_Control_Create")).unwrap()); - let ADL2_Main_Control_Destroy: ADL2_MAIN_CONTROL_DESTROY = transmute(&GetProcAddress(hDLL, s!("ADL2_Main_Control_Destroy")).unwrap()); - let ADL2_Adapter_NumberOfAdapters_Get: ADL2_ADAPTER_NUMBEROFADAPTERS_GET = transmute(&GetProcAddress(hDLL, s!("ADL2_Adapter_NumberOfAdapters_Get")).unwrap()); - - - let m: *mut c_void = libc::malloc(4); - - - } -} -*/ +// Partial implementation for PCI IDE ISA Xcelerator. +// https://www.kernel.org/doc/html/latest/i2c/summary.html + +use std::time::Duration; + +use crate::{timer, wrapper_winring0}; + +pub const I2C_BLOCK_MAX: usize = 32; + +#[repr(u16)] +#[derive(Clone, Copy, Debug)] +enum TransactionType { + I2cSmbusQuick = 0, + I2cSmbusByte = 1, + I2cSmbusByteData = 2, + I2cSmbusWordData = 3, + I2cSmbusProcCall = 4, + I2cSmbusBlockData = 5, + I2cSmbusI2cBlockBroken = 6, + I2cSmbusBlockProcCall = 7, /* SMBus 2.0 */ + I2cSmbusI2cBlockData = 8, +} + +#[repr(u16)] +#[derive(Clone, Copy, Debug)] +enum Piix4TransactionType { + Piix4Quick = 0x00, + Piix4Byte = 0x04, + Piix4ByteData = 0x08, + Piix4WordData = 0x0C, + Piix4BlockData = 0x14, +} + +// PIIX4 SMBus address offsets + +#[repr(u16)] +#[derive(Clone, Copy, Debug)] +enum SMBusAddressOffsets { + Smbhststs = 0, + Smbhslvsts = 1, + Smbhstcnt = 2, + Smbhstcmd = 3, + Smbhstadd = 4, + Smbhstdat0 = 5, + Smbhstdat1 = 6, + Smbblkdat = 7, + Smbslvcnt = 8, + Smbshdwcmd = 9, + Smbslvevt = 0xA, + Smbslvdat = 0xC, +} + +#[repr(u8)] +#[derive(Clone, Copy)] +enum AccessType { + Write = 0, + Read = 1, +} + +pub struct I2c { + base_address: u16, +} + +enum XferResult { + Ok, + BlockData(Vec), +} + +#[derive(Debug)] +enum Error { + Busy, + Timeout, + IO, + Data, +} + +impl I2c { + pub fn new(base_address: u16) -> Self { + I2c { base_address } + } + + pub fn write_block_data(&self, addr: u8, command: u8, data: &[u8]) { + let l = data.len(); + assert!( + l <= I2C_BLOCK_MAX, + "Data length must not exceed {}", + I2C_BLOCK_MAX + ); + let mut data_block = [0u8; I2C_BLOCK_MAX + 2]; + data_block[0] = l as u8; + data_block[1..l + 1].copy_from_slice(data); + + unsafe { + if let Err(error) = self.i2c_smbus_xfer( + addr, + AccessType::Write, + command, + TransactionType::I2cSmbusBlockData, + Some(&data_block), + ) { + println!("Error when writing block (I2c): {error:?}"); + } + } + } + + pub fn i2c_smbus_write_quick(&self, addr: u8, value: u8) { + unsafe { + let _ = self.i2c_smbus_xfer( + addr, + AccessType::Write, + value, + TransactionType::I2cSmbusQuick, + None, + ); + } + } + + unsafe fn i2c_smbus_xfer( + &self, + addr: u8, + access_type: AccessType, + command: u8, + transaction_type: TransactionType, // Called 'size' in 'i2c_smbus\i2c_smbus_piix4.cpp'. + data: Option<&[u8]>, + ) -> Result { + let piix4_transaction_type = match transaction_type { + TransactionType::I2cSmbusQuick => { + self.write_io_port_byte( + SMBusAddressOffsets::Smbhstadd, + (addr << 1) | access_type as u8, + ); + Piix4TransactionType::Piix4Quick + } + TransactionType::I2cSmbusByte => todo!(), + TransactionType::I2cSmbusByteData => todo!(), // Here 'data' should be a byte, maybe using a enum?. + TransactionType::I2cSmbusWordData => todo!(), // Here 'data' should be a u16, maybe using a enum?. + TransactionType::I2cSmbusBlockData => { + self.write_io_port_byte( + SMBusAddressOffsets::Smbhstadd, + (addr << 1) | access_type as u8, + ); + self.write_io_port_byte(SMBusAddressOffsets::Smbhstcmd, command); + if let AccessType::Write = access_type { + let len = data.unwrap()[0]; + if len == 0 || len > I2C_BLOCK_MAX as u8 { + panic!("Invalid len value: {}", len); + } + + self.write_io_port_byte(SMBusAddressOffsets::Smbhstdat0, len); + self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); // TODO: do something of the result!? + for i in 1..=len { + self.write_io_port_byte( + SMBusAddressOffsets::Smbblkdat, + data.unwrap()[i as usize], + ); + } + } + Piix4TransactionType::Piix4BlockData + } + _ => panic!("Not supported: {:?}", transaction_type), + }; + + self.write_io_port_byte( + SMBusAddressOffsets::Smbhstcnt, + piix4_transaction_type as u8 & 0x1C, + ); + + self.piix4_transaction()?; + + // if let (AccessType::Write, Piix4TransactionType::Piix4Quick) = (access_type, piix4_transaction_type) { + // return Ok(()) + // } + + match piix4_transaction_type { + Piix4TransactionType::Piix4Quick => Ok(XferResult::Ok), + Piix4TransactionType::Piix4Byte => todo!(), + Piix4TransactionType::Piix4ByteData => todo!(), + Piix4TransactionType::Piix4WordData => todo!(), + Piix4TransactionType::Piix4BlockData => { + let l = self.read_io_port_byte(SMBusAddressOffsets::Smbhstdat0) as usize; + if l == 0 || l > I2C_BLOCK_MAX { + return Err(Error::Data); + } + self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); + let mut data = vec![0; l + 1]; + for i in 1..=l { + data[i] = self.read_io_port_byte(SMBusAddressOffsets::Smbblkdat); + } + Ok(XferResult::BlockData(data)) + } + } + } + + unsafe fn piix4_transaction(&self) -> Result<(), Error> { + let timer = timer::Sleep::new(); + + // Make sure the SMBus is ready to start transmitting. + let mut res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); + if res != 0x00 { + self.write_io_port_byte(SMBusAddressOffsets::Smbhststs, res); + res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); + if res != 0x00 { + return Err(Error::Busy); + } + } + + // Start the transaction by setting bit 6. + res = self.read_io_port_byte(SMBusAddressOffsets::Smbhstcnt); + self.write_io_port_byte(SMBusAddressOffsets::Smbhstcnt, res | 0x40); + + // let duration: i64 = -2_500; // 250 us. + let mut n = 0; + loop { + timer.wait(Duration::from_micros(250)); + + res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); + // println!("Res: {}", res); + if res > 0x01 { + break; + } + + if n >= 100 { + return Err(Error::Timeout); + } + n += 1; + } + // println!("-----"); + + if res & 0x10 != 0x00 || res & 0x08 != 0x0 || res & 0x04 != 0x0 { + return Err(Error::IO); + } + + res = self.read_io_port_byte(SMBusAddressOffsets::Smbhststs); + if res != 0x00 { + self.write_io_port_byte(SMBusAddressOffsets::Smbhststs, res); + } + + Ok(()) + } + + unsafe fn write_io_port_byte(&self, op: SMBusAddressOffsets, value: u8) { + wrapper_winring0::WriteIoPortByte(self.base_address + op as u16, value); + } + + unsafe fn read_io_port_byte(&self, op: SMBusAddressOffsets) -> u8 { + wrapper_winring0::ReadIoPortByte(self.base_address + op as u16) + } +} + +/* +type ADL_MAIN_MALLOC_CALLBACK = unsafe fn(c_int) -> *mut c_void; +type ADL_CONTEXT_HANDLE = *mut c_void; + +type ADL2_MAIN_CONTROL_CREATE = unsafe extern "C" fn(ADL_MAIN_MALLOC_CALLBACK, c_int, *mut ADL_CONTEXT_HANDLE) -> c_int; +type ADL2_MAIN_CONTROL_DESTROY = unsafe extern "C" fn(ADL_CONTEXT_HANDLE) -> c_int; +type ADL2_ADAPTER_NUMBEROFADAPTERS_GET = unsafe extern "C" fn(ADL_CONTEXT_HANDLE, *mut c_int) -> c_int; + +pub fn test() { + unsafe { + let hDLL = LoadLibraryW(w!("atiadlxx.dll")).unwrap(); + println!("{:?}", hDLL); + + let ADL2_Main_Control_Create: ADL2_MAIN_CONTROL_CREATE = transmute(&GetProcAddress(hDLL, s!("ADL2_Main_Control_Create")).unwrap()); + let ADL2_Main_Control_Destroy: ADL2_MAIN_CONTROL_DESTROY = transmute(&GetProcAddress(hDLL, s!("ADL2_Main_Control_Destroy")).unwrap()); + let ADL2_Adapter_NumberOfAdapters_Get: ADL2_ADAPTER_NUMBEROFADAPTERS_GET = transmute(&GetProcAddress(hDLL, s!("ADL2_Adapter_NumberOfAdapters_Get")).unwrap()); + + + let m: *mut c_void = libc::malloc(4); + + + } +} +*/ diff --git a/src/rgb.rs b/src/rgb.rs index a6217c7..9152459 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -1,17 +1,17 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] -pub struct RGB { - pub red: u8, - pub green: u8, - pub blue: u8, -} - -// 'value' is between 0 and 1. -pub fn linear_interpolation(color1: RGB, color2: RGB, value: f32) -> RGB { - let red = (color1.red as f32 + (color2.red as f32 - color1.red as f32) * value) as u8; - let green = (color1.green as f32 + (color2.green as f32 - color1.green as f32) * value) as u8; - let blue = (color1.blue as f32 + (color2.blue as f32 - color1.blue as f32) * value) as u8; - - RGB { red, green, blue } -} +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct Rgb { + pub red: u8, + pub green: u8, + pub blue: u8, +} + +// 'value' is between 0 and 1. +pub fn linear_interpolation(color1: Rgb, color2: Rgb, value: f32) -> Rgb { + let red = (color1.red as f32 + (color2.red as f32 - color1.red as f32) * value) as u8; + let green = (color1.green as f32 + (color2.green as f32 - color1.green as f32) * value) as u8; + let blue = (color1.blue as f32 + (color2.blue as f32 - color1.blue as f32) * value) as u8; + + Rgb { red, green, blue } +} diff --git a/src/settings.rs b/src/settings.rs index 43ae03d..54c880b 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,64 +1,64 @@ -use std::fs::File; - -use ron::{ - de::from_reader, - ser::{to_writer_pretty, PrettyConfig}, -}; -use serde::{Deserialize, Serialize}; - -use crate::rgb::RGB; - -#[derive(Debug, Deserialize, Serialize)] -pub enum MachineName { - Jiji, - LyssMetal, - LyssMetal2, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Settings { - pub machine_name: MachineName, - pub cold_color_1: RGB, - pub hot_color_1: RGB, - pub cold_color_2: Option, - pub hot_color_2: Option, - // Average temperature between CPU and GPU. - pub cold_temperature: f32, - pub hot_temperature: f32, -} - -type Result = std::result::Result>; - -impl Settings { - fn default() -> Self { - Settings { - machine_name: MachineName::Jiji, - cold_color_1: RGB { - red: 0, - green: 255, - blue: 40, - }, - hot_color_1: RGB { - red: 255, - green: 0, - blue: 0, - }, - cold_color_2: None, - hot_color_2: None, - cold_temperature: 55., - hot_temperature: 75., - } - } - - pub fn read(file_path: &str) -> Result { - match File::open(file_path) { - Ok(file) => from_reader(file).map_err(|e| e.into()), - Err(_) => { - let file = File::create(file_path)?; - let default_config = Settings::default(); - to_writer_pretty(file, &default_config, PrettyConfig::new())?; - Ok(default_config) - } - } - } -} +use std::fs::File; + +use ron::{ + de::from_reader, + ser::{PrettyConfig, to_writer_pretty}, +}; +use serde::{Deserialize, Serialize}; + +use crate::rgb::Rgb; + +#[derive(Debug, Deserialize, Serialize)] +pub enum MachineName { + Jiji, + LyssMetal, + LyssMetal2, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Settings { + pub machine_name: MachineName, + pub cold_color_1: Rgb, + pub hot_color_1: Rgb, + pub cold_color_2: Option, + pub hot_color_2: Option, + // Average temperature between CPU and GPU. + pub cold_temperature: f32, + pub hot_temperature: f32, +} + +type Result = std::result::Result>; + +impl Settings { + fn default() -> Self { + Settings { + machine_name: MachineName::Jiji, + cold_color_1: Rgb { + red: 0, + green: 255, + blue: 40, + }, + hot_color_1: Rgb { + red: 255, + green: 0, + blue: 0, + }, + cold_color_2: None, + hot_color_2: None, + cold_temperature: 55., + hot_temperature: 75., + } + } + + pub fn read(file_path: &str) -> Result { + match File::open(file_path) { + Ok(file) => from_reader(file).map_err(|e| e.into()), + Err(_) => { + let file = File::create(file_path)?; + let default_config = Settings::default(); + to_writer_pretty(file, &default_config, PrettyConfig::new())?; + Ok(default_config) + } + } + } +} diff --git a/src/tests.rs b/src/tests.rs index c7ea81c..c0d0ad4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,221 +1,230 @@ -use std::collections::HashMap; - -use wmi::{COMLibrary, Variant, WMIConnection}; - -use crate::{ - a770, asus_aura_usb, corsair_lighting_pro, corsair_vengeance, cpu_temperature, - gigabyte_rgb_fusion_usb, lian_li_sl_infinity, machine, rgb::RGB, winring0, wrapper_winring0, -}; - -pub fn tests() { - println!("Running some tests..."); - - winring0::init(); - - // test_asus_aura_usb(asus_aura_usb::Motherboard::Asus650e); - // test_corsair_lighting_pro(); - // test_lianli_sl_infinity(); - // list_usb_devices(); - // test_roccat(); - // test_wmi(); - // test_corsair(); - // test_a770(); - // test_3080ti(); - // test_read_temperature_cpu(); - // test_read_temperature_a770(); - // test_read_temperature_3080(); - test_gigabyte_fusion(); - - winring0::deinit(); - - println!("Press any key to continue..."); - std::io::stdin().read_line(&mut String::new()).unwrap(); -} - -fn test_gigabyte_fusion() { - let api = hidapi::HidApi::new().unwrap(); - let device = gigabyte_rgb_fusion_usb::Device::new(&api).unwrap(); - // device.test_raw_data().unwrap(); - device.set_color(&RGB { - red: 0xFF, - green: 0x00, - blue: 0x00, - }); -} - -fn test_wmi() { - let com_con = COMLibrary::new().unwrap(); - let wmi_con = WMIConnection::new(com_con.into()).unwrap(); - - //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPSignedDriver WHERE Description LIKE '%SMBUS%' OR Description LIKE '%SM BUS%'").unwrap(); - //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPSignedDriver WHERE Description LIKE 'Intel(R) NF I2C Host Controller'").unwrap(); - let results: Vec> = wmi_con - .raw_query("SELECT * FROM Win32_PnPSignedDriver") - .unwrap(); - //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPAllocatedResource").unwrap(); - - for os in results { - println!("-------------------"); - println!("{:#?}", os); - } -} - -fn list_usb_devices() { - let api = hidapi::HidApi::new().unwrap(); - for device in api.device_list() { - println!("{:?}", device); - println!("name: {}", device.product_string().unwrap()); - println!("interface number: {}", device.interface_number()); - println!("page: {}", device.usage_page()); - println!("usage: {}", device.usage()); - println!("----"); - } -} - -// fn test_roccat() { -// let api = hidapi::HidApi::new().unwrap(); -// let roccat_device = roccat::get_device(&api); - -// let manufacturer = roccat_device.get_manufacturer_string().unwrap(); -// dbg!(manufacturer); - -// let product = roccat_device.get_product_string().unwrap(); -// dbg!(product); - -// let serial = roccat_device.get_serial_number_string().unwrap(); -// dbg!(serial); - -// roccat::init(&roccat_device); -// roccat::set_color( -// &roccat_device, -// &RGB { -// red: 0, -// green: 255, -// blue: 40, -// }, -// ); -// } - -fn test_asus_aura_usb(motherboard: asus_aura_usb::Motherboard) { - let api = hidapi::HidApi::new().unwrap(); - - let device = asus_aura_usb::Device::new(&api, motherboard).unwrap(); - - println!("Firmware: {}", device.get_firmware_string().unwrap()); - - let configuration = device.get_configuration_table().unwrap(); - println!("Configuration:"); - for i in 0..60 { - print!("{:02X} ", configuration[i]); - if (i + 1) % 6 == 0 { - println!(""); - } - } - println!("Number of addressable header: {}", configuration[0x02]); - println!("Number of leds: {}", configuration[0x1B]); - println!("Number of RGB headers: {}", configuration[0x1D]); - - device - .set_color(&RGB { - red: 0, - green: 0, - blue: 255, - }) - .unwrap(); - - device.save_current_color().unwrap(); -} - -fn test_corsair_lighting_pro() { - let api = hidapi::HidApi::new().unwrap(); - let device = corsair_lighting_pro::Device::new( - &api, - &RGB { - red: 0, - green: 255, - blue: 0, - }, - ); - - for i in 0..=255 { - if i % 10 == 0 || i == 255 || i == 0 { - device.set_color(&RGB { - red: i as u8, - green: 255u8 - i as u8, - blue: 0, - }); - std::thread::sleep(std::time::Duration::from_millis(200)); - } - } -} - -fn test_lianli_sl_infinity() { - let api = hidapi::HidApi::new().unwrap(); - let device = lian_li_sl_infinity::Device::new(&api); - - device.set_color(&RGB { - red: 0, - green: 0, - blue: 255, - }); -} - -fn test_corsair() { - let corsair_controllers = [ - corsair_vengeance::Controller::new(0x19), - corsair_vengeance::Controller::new(0x1B), - ]; - - for controller in corsair_controllers { - controller.set_color(&RGB { - red: 0, - green: 0, - blue: 255, - }); - } -} - -fn test_a770() { - // a770::set_rgb(255, 0, 0); - let mut a770 = a770::A770::new().unwrap(); - a770.set_color(255, 0, 0).unwrap(); -} - -fn test_3080ti() { - let machine: &mut dyn machine::Machine = - &mut machine::lyss_metal::MachineLyssMetal::new().unwrap(); - - machine.set_color(&RGB { - red: 255, - green: 0, - blue: 0, - }); -} - -const F17H_M01H_THM_TCON_CUR_TMP: u32 = 0x00059800; -const F17H_TEMP_OFFSET_FLAG: u32 = 0x80000; -const FAMILY_17H_PCI_CONTROL_REGISTER: u32 = 0x60; - -fn test_read_temperature_cpu() { - println!("temp cpu: {}", cpu_temperature::read()) -} - -fn test_read_temperature_a770() { - let jiji: &dyn machine::Machine = &machine::jiji::MachineJiji::new().unwrap(); - println!("temp gpu: {}", jiji.get_gpu_tmp()); -} - -fn test_read_temperature_3080() { - nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); - // if let Ok(gpus) = { - // for gpu in gpus { - // let thermal = gpu.thermal_settings(None).unwrap()[0]; - // println!("{:?}", thermal.current_temperature.0) - // } - // } - let gpus = nvapi::PhysicalGpu::enumerate().unwrap(); - let gpu = &gpus[0]; - let sensor = gpu.thermal_settings(None).unwrap()[0]; - println!("{:?}", sensor.current_temperature.0); - nvapi::unload().unwrap(); -} +use std::collections::HashMap; + +use wmi::{COMLibrary, Variant, WMIConnection}; + +use crate::{ + a770, asus_aura_usb, corsair_lighting_pro, corsair_vengeance, cpu_temperature, + gigabyte_rgb_fusion_usb, lian_li_sl_infinity, machine, rgb::Rgb, winring0, +}; + +pub fn tests() { + println!("Running some tests..."); + + winring0::init(); + + // test_asus_aura_usb(asus_aura_usb::Motherboard::Asus650e); + // test_corsair_lighting_pro(); + // test_lianli_sl_infinity(); + // list_usb_devices(); + // test_roccat(); + // test_wmi(); + // test_corsair(); + // test_a770(); + // test_3080ti(); + // test_read_temperature_cpu(); + // test_read_temperature_a770(); + // test_read_temperature_3080(); + test_gigabyte_fusion(); + + winring0::deinit(); + + println!("Press any key to continue..."); + std::io::stdin().read_line(&mut String::new()).unwrap(); +} + +fn test_gigabyte_fusion() { + let api = hidapi::HidApi::new().unwrap(); + let device = gigabyte_rgb_fusion_usb::Device::new(&api).unwrap(); + device.test_raw_data().unwrap(); + // device + // .set_color(&Rgb { + // red: 0xFF, + // green: 0x00, + // blue: 0x00, + // }) + // .unwrap(); +} + +fn test_wmi() { + let com_con = COMLibrary::new().unwrap(); + let wmi_con = WMIConnection::new(com_con).unwrap(); + + //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPSignedDriver WHERE Description LIKE '%SMBUS%' OR Description LIKE '%SM BUS%'").unwrap(); + //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPSignedDriver WHERE Description LIKE 'Intel(R) NF I2C Host Controller'").unwrap(); + let results: Vec> = wmi_con + .raw_query("SELECT * FROM Win32_PnPSignedDriver") + .unwrap(); + //let results: Vec> = wmi_con.raw_query("SELECT * FROM Win32_PnPAllocatedResource").unwrap(); + + for os in results { + println!("-------------------"); + println!("{:#?}", os); + } +} + +fn list_usb_devices() { + let api = hidapi::HidApi::new().unwrap(); + for device in api.device_list() { + println!("{:?}", device); + println!("name: {}", device.product_string().unwrap()); + println!("interface number: {}", device.interface_number()); + println!("page: {}", device.usage_page()); + println!("usage: {}", device.usage()); + println!("----"); + } +} + +// fn test_roccat() { +// let api = hidapi::HidApi::new().unwrap(); +// let roccat_device = roccat::get_device(&api); + +// let manufacturer = roccat_device.get_manufacturer_string().unwrap(); +// dbg!(manufacturer); + +// let product = roccat_device.get_product_string().unwrap(); +// dbg!(product); + +// let serial = roccat_device.get_serial_number_string().unwrap(); +// dbg!(serial); + +// roccat::init(&roccat_device); +// roccat::set_color( +// &roccat_device, +// &RGB { +// red: 0, +// green: 255, +// blue: 40, +// }, +// ); +// } + +fn test_asus_aura_usb(motherboard: asus_aura_usb::Motherboard) { + let api = hidapi::HidApi::new().unwrap(); + + let device = asus_aura_usb::Device::new(&api, motherboard).unwrap(); + + println!("Firmware: {}", device.get_firmware_string().unwrap()); + + let configuration = device.get_configuration_table().unwrap(); + println!("Configuration:"); + for i in 0..60 { + print!("{:02X} ", configuration[i]); + if (i + 1) % 6 == 0 { + println!(); + } + } + println!("Number of addressable header: {}", configuration[0x02]); + println!("Number of leds: {}", configuration[0x1B]); + println!("Number of RGB headers: {}", configuration[0x1D]); + + device + .set_color(&Rgb { + red: 0, + green: 0, + blue: 255, + }) + .unwrap(); + + device.save_current_color().unwrap(); +} + +fn test_corsair_lighting_pro() { + let api = hidapi::HidApi::new().unwrap(); + let device = corsair_lighting_pro::Device::new( + &api, + &Rgb { + red: 0, + green: 255, + blue: 0, + }, + ) + .unwrap(); + + for i in 0..=255 { + if i % 10 == 0 || i == 255 || i == 0 { + device + .set_color(&Rgb { + red: i as u8, + green: 255u8 - i as u8, + blue: 0, + }) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(200)); + } + } +} + +fn test_lianli_sl_infinity() { + let api = hidapi::HidApi::new().unwrap(); + let device = lian_li_sl_infinity::Device::new(&api).unwrap(); + + device + .set_color(&Rgb { + red: 0, + green: 0, + blue: 255, + }) + .unwrap(); +} + +fn test_corsair() { + let corsair_controllers = [ + corsair_vengeance::Controller::new(0x19), + corsair_vengeance::Controller::new(0x1B), + ]; + + for controller in corsair_controllers { + controller.set_color(&Rgb { + red: 0, + green: 0, + blue: 255, + }); + } +} + +fn test_a770() { + // a770::set_rgb(255, 0, 0); + let mut a770 = a770::A770::new().unwrap(); + a770.set_color(255, 0, 0).unwrap(); +} + +fn test_3080ti() { + let machine: &mut dyn machine::Machine = + &mut machine::lyss_metal::MachineLyssMetal::new().unwrap(); + + machine + .set_color(&Rgb { + red: 255, + green: 0, + blue: 0, + }) + .unwrap(); +} + +const F17H_M01H_THM_TCON_CUR_TMP: u32 = 0x00059800; +const F17H_TEMP_OFFSET_FLAG: u32 = 0x80000; +const FAMILY_17H_PCI_CONTROL_REGISTER: u32 = 0x60; + +fn test_read_temperature_cpu() { + println!("temp cpu: {}", cpu_temperature::read()) +} + +fn test_read_temperature_a770() { + let jiji: &dyn machine::Machine = &machine::jiji::MachineJiji::new().unwrap(); + println!("temp gpu: {}", jiji.get_gpu_tmp()); +} + +fn test_read_temperature_3080() { + nvapi::initialize().expect("Unable to initialize nvapi (Nvidia API)"); + // if let Ok(gpus) = { + // for gpu in gpus { + // let thermal = gpu.thermal_settings(None).unwrap()[0]; + // println!("{:?}", thermal.current_temperature.0) + // } + // } + let gpus = nvapi::PhysicalGpu::enumerate().unwrap(); + let gpu = &gpus[0]; + let sensor = gpu.thermal_settings(None).unwrap()[0]; + println!("{:?}", sensor.current_temperature.0); + nvapi::unload().unwrap(); +} -- 2.50.0 From aed6ea81c17b58c52a52be2c29d9b2344eb45803 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Wed, 19 Mar 2025 18:26:54 +0100 Subject: [PATCH 5/6] Update winring --- winring0/OlsApi.h | 44 --------------------------------------- winring0/WinRing0x64.dll | Bin 29696 -> 62976 bytes winring0/WinRing0x64.lib | Bin 15120 -> 15124 bytes winring0/WinRing0x64.sys | Bin 11512 -> 14544 bytes 4 files changed, 44 deletions(-) diff --git a/winring0/OlsApi.h b/winring0/OlsApi.h index 81ebdfd..ff72235 100644 --- a/winring0/OlsApi.h +++ b/winring0/OlsApi.h @@ -442,17 +442,6 @@ WINAPI ReadPciConfigDword( BYTE regAddress // Configuration Address 0-255 ); -//----------------------------------------------------------------------------- -// ReadPciConfigPByte -//----------------------------------------------------------------------------- -BOOL // Read Value -WINAPI ReadPciConfigPByte( - DWORD pciAddress, // PCI Device Address - DWORD regAddress, // Configuration Address 0-whatever - PBYTE value, // Read Value - DWORD size -); - //----------------------------------------------------------------------------- // ReadPciConfigByteEx //----------------------------------------------------------------------------- @@ -483,17 +472,6 @@ WINAPI ReadPciConfigDwordEx( PDWORD value // Read Value ); -//----------------------------------------------------------------------------- -// ReadPciConfigPByteEx -//----------------------------------------------------------------------------- -BOOL // TRUE: success, FALSE: failure -WINAPI ReadPciConfigPByteEx( - DWORD pciAddress, // PCI Device Address - DWORD regAddress, // Configuration Address 0-whatever - PBYTE value, // Read Value - DWORD size -); - //----------------------------------------------------------------------------- // WritePciConfigByte //----------------------------------------------------------------------------- @@ -524,17 +502,6 @@ WINAPI WritePciConfigDword( DWORD value // Write Value ); -//----------------------------------------------------------------------------- -// WritePciConfigPByte -//----------------------------------------------------------------------------- -VOID -WINAPI WritePciConfigPByte( - DWORD pciAddress, // PCI Device Address - BYTE regAddress, // Configuration Address 0-255 - PBYTE value, // Write Value - DWORD size -); - //----------------------------------------------------------------------------- // WritePciConfigByteEx //----------------------------------------------------------------------------- @@ -565,17 +532,6 @@ WINAPI WritePciConfigDwordEx( DWORD value // Write Value ); -//----------------------------------------------------------------------------- -// WritePciConfigPByte -//----------------------------------------------------------------------------- -BOOL // TRUE: success, FALSE: failure -WINAPI WritePciConfigPByteEx( - DWORD pciAddress, // PCI Device Address - BYTE regAddress, // Configuration Address 0-255 - PBYTE value, // Write Value - DWORD size -); - //----------------------------------------------------------------------------- // FindPciDeviceById //----------------------------------------------------------------------------- diff --git a/winring0/WinRing0x64.dll b/winring0/WinRing0x64.dll index 2027ee6706702c9f6f840ce5bc6e34b412ec0606..4a48c7a1f6c9945906e9507fce4077facb69821a 100644 GIT binary patch literal 62976 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 literal 11512 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 -- 2.50.0 From 17a46e844e3792433ed955d215f3cc03f3400816 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Fri, 4 Apr 2025 15:46:55 +0200 Subject: [PATCH 6/6] Add Mshroom machine implementation --- src/machine/mod.rs | 1 + src/machine/mshroom.rs | 45 ++++++++++++++++++++++++++++++++++++++++++ src/main_loop.rs | 3 +++ src/settings.rs | 3 ++- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/machine/mshroom.rs diff --git a/src/machine/mod.rs b/src/machine/mod.rs index f6cddfe..fd8b84f 100644 --- a/src/machine/mod.rs +++ b/src/machine/mod.rs @@ -3,6 +3,7 @@ use crate::rgb; pub mod jiji; pub mod lyss_metal; pub mod lyss_metal2; +pub mod mshroom; const RGB_FUSION2_GPU_REG_COLOR: u8 = 0x40; const RGB_FUSION2_GPU_REG_MODE: u8 = 0x88; diff --git a/src/machine/mshroom.rs b/src/machine/mshroom.rs new file mode 100644 index 0000000..9aa7559 --- /dev/null +++ b/src/machine/mshroom.rs @@ -0,0 +1,45 @@ +use crate::{corsair_vengeance, cpu_temperature, rgb}; + +use super::Machine; + +pub struct Mshroom { + ram: Vec, +} + +impl Mshroom { + pub fn new() -> anyhow::Result { + let machine = Self { + ram: vec![ + corsair_vengeance::Controller::new(0x19), + corsair_vengeance::Controller::new(0x1B), + ], + }; + + Ok(machine) + } +} + +impl Machine for Mshroom { + fn set_color_1(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + for controller in &self.ram { + controller.set_color(color); + } + Ok(()) + } + + fn set_color_2(&mut self, color: &rgb::Rgb) -> anyhow::Result<()> { + Ok(()) + } // No color 2. + + fn get_gpu_tmp(&self) -> f32 { + // unsafe { intel_arc::GetTemperature(self.gpu_devices, 0) as f32 } + // self.gpus[0].thermal_settings(None).unwrap()[0] + // .current_temperature + // .0 as f32 + 0. + } + + fn get_cpu_tmp(&self) -> f32 { + cpu_temperature::read() + } +} diff --git a/src/main_loop.rs b/src/main_loop.rs index a8b7618..8e88ecf 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -51,6 +51,9 @@ pub fn main_loop(completed: Arc) { machine::lyss_metal2::MachineLyssMetal2::new() .expect("Unable to create MachineLyssMetal2"), ), + settings::MachineName::Mshroom => { + Box::new(machine::mshroom::Mshroom::new().expect("Unable to create Mshroom")) + } }; let mut kernel = [0f32; consts::KERNEL_SIZE_SAMPLES]; diff --git a/src/settings.rs b/src/settings.rs index 54c880b..feaa201 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -13,6 +13,7 @@ pub enum MachineName { Jiji, LyssMetal, LyssMetal2, + Mshroom, } #[derive(Debug, Deserialize, Serialize)] @@ -32,7 +33,7 @@ type Result = std::result::Result>; impl Settings { fn default() -> Self { Settings { - machine_name: MachineName::Jiji, + machine_name: MachineName::Mshroom, cold_color_1: Rgb { red: 0, green: 255, -- 2.50.0