diff --git a/Cargo.toml b/Cargo.toml index 5de493e..9d8c236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,15 @@ [workspace] +members = ["lib", "cli", "gui"] +default-members = ["gui"] -members = [ - "lib", "cli", "gui" -] +[workspace.package] +edition = "2021" +version = "0.2.0" +authors = ["George Poulios"] +description = "A utility to control Razer DeathAdder v2 on Windows" +readme = "./README.md" +repository = "https://github.com/gpoulios/deathadderv2" +license = "GPLv2" + +[workspace.dependencies] +rgb = { version = "0.8.36" } \ No newline at end of file diff --git a/README.md b/README.md index 9f60796..b83df83 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,32 @@ -# deathadderv2-rgb +# deathadderv2 Set (a constant) color on the Razer DeathAdder v2. (while practising in rust) -A little utility for those of us that don't want to run 2-3 apps and 6 services (!!) just for keeping this mouse from changing colors like a christmas tree. Personally, I don't care about the auto-switching of profiles that Synapse provides and all the other functionality I can have without running Razer's apps in the background. +A little utility for those of us that don't want to run 2-3 apps and 6 services (!!) just for keeping this mouse from changing colors like a christmas tree. I don't need the auto-switching of profiles that Synapse provides and (most if not all) the other functionality I can have without running Razer's apps in the background. -Unfortunately, the device does not remember the color and it comes back with the rainbow on power on (either after sleep/hibernation or on boot). Not going to bother making it into a service as the task scheduler suits me just fine (read below if you're interested in maintaining the setting). +Device protocol largely ported from [openrazer](https://github.com/openrazer/openrazer). So far I've ported all of the functionality for this particular mouse except wave/breath/spectrum/whatnot effects. The plan is to write a small UI to control settings like DPI, poll rate and brightness before I integrate RGB effects, if ever. ## Requirements -This is not for supposed to be for Linux hosts. If you are on Linux, see [openrazer](https://github.com/openrazer/openrazer). +This is not supposed to be for Linux hosts. If you are on Linux, see [openrazer](https://github.com/openrazer/openrazer), it's a great project, and supports many more features, as well as almost all devices. -Windows users, only requirement is to be using the [libusb driver](https://github.com/libusb/libusb/wiki/Windows) (either WinUSB or libusb-win32). +For Windows users, the only requirement is to be using the [libusb driver](https://github.com/libusb/libusb/wiki/Windows) (either WinUSB or libusb-win32). -One way to install it is using [Zadig](https://zadig.akeo.ie/). You only need to do this once. Change the entry "Razer DeathAdder V2 (Interface 3)". Use the spinner to select either "WinUSB (vXXX)" or "libusb-win32 (vX.Y.Z)" and hit "Replace driver". In my case (Win11) it timed out while creating a restore point but it actually installed it. +One way to install it is using [Zadig](https://zadig.akeo.ie/). You only need to do this once. Change the entry "Razer DeathAdder V2 (Interface 3)" by using the spinner to select either "WinUSB (vXXX)" or "libusb-win32 (vX.Y.Z)" and hitting "Replace driver". In my case (Win11) it timed out while creating a restore point but it actually installed it. ## Usage The tool comes in two forms, a console executable that you can use like so: ``` -> deathadder-rgb-cli.exe aabbcc -``` - -and a GUI app that will just pop up a color picker prompt (check the mouse while selecting). - -You can use the GUI version with command line arguments too (same usage as above), except a console window will not be allocated (this is intentional). - -I have not found a way to retrieve the current color from the device so both apps will save the last sent color to a file under %APPDATA%/deathadder/config/default-config.toml. - -### Bonus - -It is actually possible to set a different color on the scroll wheel (Synapse doesn't support this at the time of this writing). But there's a catch: most combinations don't work and I don't understand why. For sure it accepts combinations when the RGB components in both colors are the same even if in different order. For instance, the following will work: +> deathadder-rgb-cli.exe [COLOR|LOGO_COLOR] [SCROLL_WHEEL_COLOR] +where *COLOR above should be in hex [0x/#]RGB[h] or [0x/#]RRGGBB[h] format. If no arguments are specified the saved color will be applied. If scroll wheel color is not specified, the specified color will be applied to both the logo and the scroll wheel. ``` -> deathadder-rgb-cli.exe 1bc c1b -> deathadder-rgb-cli.exe 1155AA AA5511 -> deathadder-rgb-cli.exe 10f243 f24310 -``` - -Edit: apparently I'm missing a simple XOR kind-of checksum calculation in the USB report packet, which, for the following combinations, ends up the same (!). Thanks [openrazer](https://github.com/openrazer/openrazer). - -### Task Scheduler: re-applying the setting - -The GUI version also supports `--last` as the first argument in which case it sets the last applied color (either from cli or gui). This is useful if you want to schedule a task that does not pop up any windows. - -A tested setup is to set a trigger at log on, and for waking up from sleep, a custom trigger on Power-Troubleshooter with event ID 1 and delay 5 seconds. In Action tab use the absolute path to `deathadder-rgb-gui.exe` and in the arguments put `--last`. I've added the (redacted) xml to the task I used in case you want to try importing it; just make sure to edit the required fields therein, it is not supposed to work as is. - -## Technical -I captured the USB using UsbPcap while Synapse was sending the color-setting commands (it was a single control transfer-write, multiple times to provide that fade effect) and replaced the RGB values in it. The rest of the packet is identical. Haven't tested in any mouse other than mine; not sure if there's anything device-specific in there that would prevent others from using it. +and a GUI app that will just pop up a color picker prompt to preview and/or set both logo and scroll wheel colors (to the same value). -The USB message header was: - -``` -Setup Data - bmRequestType: 0x21 - bRequest: SET_REPORT (0x09) - wValue: 0x0300 - wIndex: 0 - wLength: 90 - Data Fragment: 001f[...] -``` - -And this is would be the payload for setting the color to bright white: - -``` -File: lib/src/lib.rs - -[...] -// the start (no idea what they are) -0x00, 0x1f, 0x00, 0x00, 0x00, 0x0b, 0x0f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, - -// wheel RGB (3B) | body RGB (3B) -0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - -// the trailer (no idea what they are either) -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00 - -[...] -``` +Contrary to all other settings, I have not found a way to retrieve the current color from the device so both apps will save the last applied color to a file under %APPDATA%/deathadder/config/default-config.toml. +--- +The project is licensed under the GPL. \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml index fa635f7..d7e9de0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,12 +1,16 @@ [package] name = "deathadder-rgb-cli" -version = "0.1.0" -edition = "2021" +edition = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +description = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [[bin]] name = "deathadder-rgb-cli" path = "src/cli.rs" [dependencies] -libdeathadder = { path = "../lib" } -rgb = "0.8.36" \ No newline at end of file +librazer = { path = "../lib" } +rgb = { workspace = true } \ No newline at end of file diff --git a/cli/src/cli.rs b/cli/src/cli.rs index ca4e349..13494ae 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,8 +1,34 @@ use rgb::RGB8; -use libdeathadder::core::{rgb_from_hex, Config}; -use libdeathadder::v2::set_color; +use librazer::cfg::Config; +use librazer::common::rgb_from_hex; +use librazer::device::{DeathAdderV2, RazerMouse}; fn main() { + // let dav2 = DeathAdderV2::new().expect("failed to open device"); + // println!("{}", dav2); + // // dav2.set_dpi(10000, 10000); + // // dav2.set_poll_rate(librazer::common::PollingRate::Hz250); + // // println!("{:?}", dav2.get_dpi()); + // // println!("{:?}", dav2.get_poll_rate()); + // // dav2.set_dpi(20000, 20000); + // // dav2.set_poll_rate(librazer::common::PollingRate::Hz1000); + // // println!("{:?}", dav2.get_dpi()); + // // println!("{:?}", dav2.get_poll_rate()); + + // let rgb1 = RGB8::from([0x00, 0xaa, 0xaa]); + // let rgb2 = RGB8::from([0xaa, 0xaa, 0x00]); + // // dav2.preview_static(rgb1, rgb2); + // dav2.set_logo_color(rgb2); + // dav2.set_scroll_color(rgb1); + + // // let init_brightness = dav2.get_logo_brightness().unwrap(); + // // println!("logo brightness: {:?}, scroll: {:?}", init_brightness, dav2.get_scroll_brightness()); + // // dav2.set_logo_brightness(30); + // // dav2.set_scroll_brightness(30); + // // println!("logo brightness: {:?}, scroll: {:?}", dav2.get_logo_brightness(), dav2.get_scroll_brightness()); + + // return; + let args: Vec = std::env::args().collect(); let parse_arg = |input: &str| -> RGB8 { @@ -14,21 +40,34 @@ fn main() { } }; - let (color, wheel_color) = match args.len() { + let (logo_color, scroll_color) = match args.len() { ..=1 => { match Config::load() { - Some(cfg) => (cfg.color, cfg.wheel_color), + Some(cfg) => (cfg.color, cfg.scroll_color.or(Some(cfg.color)).unwrap()), None => panic!("failed to load configuration; please specify \ arguments manually") } }, - 2 => (parse_arg(args[1].as_ref()), None), - 3 => (parse_arg(args[1].as_ref()), Some(parse_arg(args[2].as_ref()))), + 2..=3 => { + let color = parse_arg(args[1].as_ref()); + (color, if args.len() == 3 { + parse_arg(args[2].as_ref()) + } else { + color + }) + }, _ => panic!("usage: {} [(body) color] [wheel color]", args[0]) }; - match set_color(color, wheel_color) { - Ok(msg) => println!("{}", msg), - Err(e) => panic!("Failed to set color(s): {}", e) - } + let dav2 = DeathAdderV2::new().expect("failed to open device"); + + _= dav2.set_logo_color(logo_color) + .map_err(|e| panic!("failed to set logo color: {}", e)) + .and_then(|_| dav2.set_scroll_color(scroll_color)) + .map_err(|e| panic!("failed to set scroll color: {}", e)); + + _ = Config { + color: logo_color, + scroll_color: Some(scroll_color), + }.save().map_err(|e| panic!("failed to save config: {}", e)); } diff --git a/gui/Cargo.toml b/gui/Cargo.toml index e9116ee..88cf6e6 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -1,16 +1,20 @@ [package] name = "deathadder-rgb-gui" -version = "0.1.0" -edition = "2021" +edition = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +description = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [[bin]] name = "deathadder-rgb-gui" path = "src/gui.rs" [dependencies] -libdeathadder = { path = "../lib" } +librazer = { path = "../lib" } winapi = "0.3.9" -rgb = "0.8.36" +rgb = { workspace = true } [dependencies.windows] version = "0.44.0" diff --git a/gui/src/gui.rs b/gui/src/gui.rs index e6916f6..da77d65 100644 --- a/gui/src/gui.rs +++ b/gui/src/gui.rs @@ -14,25 +14,14 @@ use windows::{ }, Controls::Dialogs::* }, - System::Diagnostics::Debug::OutputDebugStringA }, }; use rgb::RGB8; -use libdeathadder::core::{rgb_from_hex, Config}; -use libdeathadder::v2::{preview_color, set_color}; +use librazer::cfg::Config; +use librazer::device::{DeathAdderV2, RazerMouse}; /* - * This utility operates in either command line mode or UI mode. - * - * In command line mode the colors are specified as cmd line arguments - * and no UI is shown. The reason for this mode is to be able to automate or - * schedule this tool using the task scheduler or smth. If this were a true - * console application (without the #![windows_subsystem = "windows"] directive - * above), in such scenarios (e.g. scheduled task) a console window would pop - * up for a split second. We abandon console support to avoid that, and we - * log error messages to the debugger using the OutputDebugString API. - * * In UI mode we show a ChooseColor dialog and let the user pick the color * while previewing the current selection on the mouse itself. * @@ -57,75 +46,48 @@ static RGB_TO_SET: Mutex> = Mutex::new(None); * Log messages to the debugger using OutputDebugString (only for command line * invocation). Use DebugView by Mark Russinovich to view */ -macro_rules! dbglog { +// macro_rules! dbglog { +// ($($args: tt)*) => { +// unsafe { +// let msg = format!($($args)*); +// OutputDebugStringA(PCSTR::from_raw(msg.as_ptr())); +// } +// } +// } + +// macro_rules! dbgpanic { +// ($($args: tt)*) => { +// unsafe { +// let msg = format!($($args)*); +// OutputDebugStringA(PCSTR::from_raw(msg.as_ptr())); +// panic!("{}", msg); +// } +// } +// } + +macro_rules! msgboxpanic { ($($args: tt)*) => { unsafe { let msg = format!($($args)*); - OutputDebugStringA(PCSTR::from_raw(msg.as_ptr())); - } - } -} - -macro_rules! dbgpanic { - ($($args: tt)*) => { - unsafe { - let msg = format!($($args)*); - OutputDebugStringA(PCSTR::from_raw(msg.as_ptr())); + let msg_ptr = PCSTR::from_raw(msg.as_ptr()); + MessageBoxA(HWND(0), msg_ptr, s!("Error"), MB_OK | MB_ICONERROR); panic!("{}", msg); } } } fn main() { - - /* - * Command line mode if at least one argument - */ - let args: Vec = std::env::args().collect(); - - let parse_arg = |input: &str| -> RGB8 { - match rgb_from_hex(input) { - Ok(rgb) => rgb, - Err(e) => { dbgpanic!("argument '{}' should be in the \ - form [0x/#]RGB[h] or [0x/#]RRGGBB[h] where R, G, and B are hex \ - digits: {}", input, e); } - } - }; - - if args.len() > 1 { - let (color, wheel_color) = if args[1] == "--last" { - match Config::load() { - Some(cfg) => (cfg.color, cfg.wheel_color), - None => dbgpanic!("failed to load configuration; please specify \ - arguments manually") - } - } else { - (parse_arg(args[1].as_ref()), if args.len() > 2 { - Some(parse_arg(args[2].as_ref())) - } else { - None - }) - }; - - match set_color(color, wheel_color) { - Ok(msg) => dbglog!("{}", msg), - Err(e) => dbgpanic!("Failed to set color(s): {}", e) - } - return; - }; - - - /* - * no arguments; UI mode - */ + let dav2 = DeathAdderV2::new() + .unwrap_or_else(|e| msgboxpanic!("Error opening device: {}", e)); // this will be the master signal to end the device preview thread let keep_previewing = Arc::new(Mutex::new(true)); - + let dav2_rc = Arc::new(dav2); let preview_thread = { // make a copy of the master signal and loop on it let keep_previewing = Arc::clone(&keep_previewing); + let dav2_rc = Arc::clone(&dav2_rc); thread::spawn(move || { // save some resources by setting each color once @@ -134,11 +96,18 @@ fn main() { while *keep_previewing.lock().unwrap() { match *RGB_TO_SET.lock().unwrap() { + // same as last set color: do nothing same if same == last_set => (), + + // would like this to be matched in arm above but it doesn't None => (), + + // some new color to preview Some(rgb) => { - _ = preview_color(rgb, None); - last_set = Some(rgb); + match (*dav2_rc).preview_static(rgb, rgb) { + Ok(()) => last_set = Some(rgb), + Err(_) => break + } }, } @@ -164,23 +133,22 @@ fn main() { *keep_previewing.lock().unwrap() = false; preview_thread.join().unwrap(); - // set the final value based on user's selection - let result = if chosen.is_some() { - set_color(chosen.unwrap(), None) - } else if cfg.is_some() { - let cfgu = cfg.unwrap(); - set_color(cfgu.color, cfgu.wheel_color) + // final value based on user's choice + let (logo_rgb, scroll_rgb) = if chosen.is_some() { + (chosen.unwrap(), chosen.unwrap()) } else { - set_color(initial, None) + (initial, initial) }; - // show error, if any - if result.is_err() { - unsafe { - let message = PCSTR::from_raw(result.unwrap().as_ptr()); - MessageBoxA(HWND(0), message, s!("Error"), MB_OK | MB_ICONERROR); - } - } + _ = (*dav2_rc).set_logo_color(logo_rgb) + .map_err(|e| msgboxpanic!("Error setting logo color: {}", e)) + .and_then(|_| (*dav2_rc).set_scroll_color(scroll_rgb)) + .map_err(|e| msgboxpanic!("Error setting scroll wheel color: {}", e)); + + _ = Config { + color: logo_rgb, + scroll_color: Some(scroll_rgb), + }.save().map_err(|e| msgboxpanic!("Failed to save config: {}", e)); } /* diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 27f15e6..45e8d7d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,10 +1,13 @@ [package] -name = "libdeathadder" -version = "0.1.0" -edition = "2021" +name = "librazer" +description = "A partial port of openrazer's driver for DeathAdder v2" +edition = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } [dependencies] -confy = "0.5.1" +rusb = "0.9" serde = { version = "1.0.152", features = ["derive"] } -rgb = { version = "0.8.36", features = ["serde"] } -rusb = "0.9" \ No newline at end of file +rgb = { workspace = true, features = ["serde"] } +confy = "0.5.1" \ No newline at end of file diff --git a/lib/src/cfg.rs b/lib/src/cfg.rs new file mode 100644 index 0000000..59db5ef --- /dev/null +++ b/lib/src/cfg.rs @@ -0,0 +1,32 @@ +use std::default::Default; +use serde::{Serialize, Deserialize}; +use confy::ConfyError; +use rgb::RGB8; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub color: RGB8, + pub scroll_color: Option, +} + +impl Config { + pub fn save(&self) -> Result<(), ConfyError> { + confy::store("deathadder_v2", None, self) + } + + pub fn load() -> Option { + match confy::load("deathadder_v2", None) { + Ok(cfg) => Some(cfg), + Err(_) => None + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + color: RGB8::new(0xAA, 0xAA, 0xAA), + scroll_color: None + } + } +} \ No newline at end of file diff --git a/lib/src/device.rs b/lib/src/device.rs new file mode 100644 index 0000000..a443d21 --- /dev/null +++ b/lib/src/device.rs @@ -0,0 +1,224 @@ +use std::{time::Duration, fmt}; +use rusb::{Context, UsbContext, DeviceHandle}; +use rgb::RGB8; +use crate::cfg::Config; +use crate::error::{USBResult, USBError}; +use crate::common::*; + +pub(crate) const USB_VENDOR_ID_RAZER: u16 = 0x1532; +pub(crate) const USB_DEVICE_ID_RAZER_DEATHADDER_V2: u16 = 0x0084; + +pub trait RazerDevice: fmt::Display { + fn vid(&self) -> u16 { USB_VENDOR_ID_RAZER } + + fn pid(&self) -> u16; + + fn name(&self) -> String; + + fn handle(&self) -> &DeviceHandle; + + fn default_tx_id(&self) -> u8; + + fn send_payload(&self, request: &mut RazerReport) -> USBResult { + request.transaction_id = self.default_tx_id(); + razer_send_payload(self.handle(), request) + } + + fn get_serial(&self) -> USBResult { + let mut request = razer_chroma_standard_get_serial(); + let response = self.send_payload(&mut request)?; + + let bytes = response.arguments[..22].iter() + .take_while(|&&c| c != 0) + .cloned() + .collect::>(); + + Ok(String::from_utf8(bytes).unwrap_or(String::from(""))) + } +} + +/// A default implementation; Some mice need specialization +pub trait RazerMouse: RazerDevice { + fn get_dpi(&self) -> USBResult<(u16, u16)> { + let mut request = razer_chroma_misc_get_dpi_xy(LedStorage::NoStore); + let response = self.send_payload(&mut request)?; + + let dpi_x = ((response.arguments[1] as u16) << 8) | (response.arguments[2] as u16) & 0xff; + let dpi_y = ((response.arguments[3] as u16) << 8) | (response.arguments[4] as u16) & 0xff; + + Ok((dpi_x, dpi_y)) + } + + fn set_dpi(&self, dpi_x: u16, dpi_y: u16) -> USBResult<()> { + let mut request = razer_chroma_misc_set_dpi_xy( + LedStorage::NoStore, dpi_x, dpi_y); + self.send_payload(&mut request)?; + Ok(()) + } + + fn get_poll_rate(&self) -> USBResult { + let mut request = razer_chroma_misc_get_polling_rate(); + let response = self.send_payload(&mut request)?; + PollingRate::try_from(response.arguments[0]) + .or(Err(USBError::ResponseUnknownValue(response.arguments[0]))) + } + + fn set_poll_rate(&self, poll_rate: PollingRate) -> USBResult<()> { + let mut request = razer_chroma_misc_set_polling_rate(poll_rate); + self.send_payload(&mut request)?; + Ok(()) + } + + fn preview_static(&self, logo_color: RGB8, scroll_color: RGB8) -> USBResult<()>; + + fn set_logo_color(&self, color: RGB8) -> USBResult<()> { + let mut request = razer_chroma_extended_matrix_effect_static( + LedStorage::VarStore, Led::Logo, color); + self.send_payload(&mut request)?; + Ok(()) + } + + fn set_scroll_color(&self, color: RGB8) -> USBResult<()> { + let mut request = razer_chroma_extended_matrix_effect_static( + LedStorage::VarStore, Led::ScrollWheel, color); + self.send_payload(&mut request)?; + Ok(()) + } + + fn get_logo_brightness(&self) -> USBResult { + let mut request = razer_chroma_extended_matrix_get_brightness( + LedStorage::VarStore, Led::Logo); + + let response = self.send_payload(&mut request)?; + Ok((100.0 * response.arguments[2] as f32 / 255.0).round() as u8) + } + + fn set_logo_brightness(&self, brightness: u8) -> USBResult<()> { + let mut request = razer_chroma_extended_matrix_brightness( + LedStorage::VarStore, Led::Logo, brightness); + self.send_payload(&mut request)?; + Ok(()) + } + + fn get_scroll_brightness(&self) -> USBResult { + let mut request = razer_chroma_extended_matrix_get_brightness( + LedStorage::VarStore, Led::ScrollWheel); + + let response = self.send_payload(&mut request)?; + Ok((100.0 * response.arguments[2] as f32 / 255.0).round() as u8) + } + + fn set_scroll_brightness(&self, brightness: u8) -> USBResult<()> { + let mut request = razer_chroma_extended_matrix_brightness( + LedStorage::VarStore, Led::ScrollWheel, brightness); + self.send_payload(&mut request)?; + Ok(()) + } + +} + +/// A default "to_string()" implementation for all RazerDevices +fn razer_dev_default_fmt(dev: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let serial = dev.get_serial().unwrap_or(String::from("")); + write!(f, "Razer {} ({})", dev.name(), serial) +} + +pub struct DeathAdderV2 { + handle: DeviceHandle, +} + +impl RazerDevice for DeathAdderV2 { + fn pid(&self) -> u16 { USB_DEVICE_ID_RAZER_DEATHADDER_V2 } + + fn name(&self) -> String { + String::from("DeathAdder v2") + } + + fn handle(&self) -> &DeviceHandle { + &self.handle + } + + fn default_tx_id(&self) -> u8 { + 0x3f // except for razer_naga_trinity_effect_static which is 0x1f + } +} + +impl RazerMouse for DeathAdderV2 { + fn preview_static(&self, logo_color: RGB8, scroll_color: RGB8) -> USBResult<()> { + let mut request = razer_naga_trinity_effect_static( + LedStorage::NoStore, LedEffect::Static, logo_color, scroll_color); + self.send_payload(&mut request)?; + Ok(()) + } +} + +impl fmt::Display for DeathAdderV2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + razer_dev_default_fmt(self, f) + } +} + +impl DeathAdderV2 { + pub fn new() -> USBResult { + let ctx = Context::new()?; + let handle = match ctx.open_device_with_vid_pid( + USB_VENDOR_ID_RAZER, USB_DEVICE_ID_RAZER_DEATHADDER_V2) { + Some(handle) => Ok(handle), + None => Err(USBError::DeviceNotFound), + }?; + Ok(Self { handle: handle }) + } +} + +pub fn preview_color(color: RGB8, wheel_color: Option) -> Result { + _set_color(color, wheel_color, false) +} + +pub fn set_color(color: RGB8, wheel_color: Option) -> Result { + _set_color(color, wheel_color, true) +} + +fn _set_color(color: RGB8, wheel_color: Option, save: bool) -> Result { + let vid = 0x1532; + let pid = 0x0084; + + let timeout = Duration::from_secs(1); + + // save regardless of USB result and fail silently + if save { + _ = Config {color, scroll_color: wheel_color}.save(); + } + + match Context::new() { + Ok(context) => match context.open_device_with_vid_pid(vid, pid) { + Some(handle) => { + + let mut packet: Vec = vec![ + // the start (no idea what they are) + 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0b, 0x0f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, + + // wheel RGB (3B) | body RGB (3B) + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + + // the trailer (no idea what they are either) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00 + ]; + + packet.splice(16..19, color.iter()); + packet.splice(13..16, wheel_color.unwrap_or(color).iter()); + + match handle.write_control(0x21, 9, 0x300, 0, + &packet, timeout) { + Ok(len) => Ok(format!("written {} bytes", len)), + Err(e) => Err(format!("could not write ctrl transfer: {}", e)) + } + } + None => Err(format!("could not find device {:04x}:{:04x}", vid, pid)), + }, + Err(e) => Err(format!("could not initialize libusb: {}", e)), + } +} \ No newline at end of file diff --git a/lib/src/error.rs b/lib/src/error.rs new file mode 100644 index 0000000..71789d7 --- /dev/null +++ b/lib/src/error.rs @@ -0,0 +1,94 @@ +use std::{num::ParseIntError, fmt, result, error}; + +#[derive(Debug)] +pub enum ParseRGBError { + WrongLength(usize), + ParseHex(ParseIntError), +} + +impl fmt::Display for ParseRGBError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ParseRGBError::WrongLength(len) => + write!(f, "excluding pre/suffixes, \ + string can only be of length 3 or 6 ({} given)", len), + ParseRGBError::ParseHex(ref pie) => + write!(f, "{}", pie), + } + } +} + +impl error::Error for ParseRGBError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + ParseRGBError::WrongLength(_) => None, + ParseRGBError::ParseHex(ref pie) => Some(pie), + } + } +} + +impl From for ParseRGBError { + fn from(err: ParseIntError) -> ParseRGBError { + ParseRGBError::ParseHex(err) + } +} + +/// A result of a function that may return a `Error`. +pub type USBResult = result::Result; + +#[derive(Debug)] +pub enum USBError { + DeviceNotFound, + /// (total, written) An incomplete write + IncompleteWrite(usize, usize), + /// (total, read) An incomplete read + IncompleteRead(usize, usize), + ResponseMismatch, + DeviceBusy, + CommandFailed, + CommandNotSupported, + CommandTimeout, + ResponseUnknownStatus(u8), + ResponseUnknownValue(u8), + /// Wrapper for rusb::Error + RUSBError(rusb::Error), +} + +impl fmt::Display for USBError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + USBError::DeviceNotFound => write!(f, "device not found"), + USBError::IncompleteWrite(total, written) => + write!(f, "failed to write full control message \ + (written {} out of {} bytes)", written, total), + USBError::IncompleteRead(total, read) => + write!(f, "failed to read full control message \ + (read {} out of {} bytes)", read, total), + USBError::ResponseMismatch => write!(f, "wrong response type"), + USBError::DeviceBusy => write!(f, "device is busy"), + USBError::CommandFailed => write!(f, "command failed"), + USBError::CommandNotSupported => write!(f, "command not supported"), + USBError::CommandTimeout => write!(f, "command timed out"), + USBError::ResponseUnknownStatus(status) => + write!(f, "unrecognized status in response: {:#02X}", status), + USBError::ResponseUnknownValue(value) => + write!(f, "unrecognized value in response: {:#02X}", value), + USBError::RUSBError(ref e) => write!(f, "{}", e), + } + } +} + +impl error::Error for USBError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + USBError::RUSBError(ref e) => Some(e), + _ => None + } + } +} + +impl From for USBError { + fn from(err: rusb::Error) -> USBError { + USBError::RUSBError(err) + } +} \ No newline at end of file diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0ffb9ae..845a24c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,69 +1,13 @@ -pub mod core { - use std::{num::ParseIntError, fmt, error, default::Default}; - use serde::{Serialize, Deserialize}; - use confy::{ConfyError}; - use rgb::{RGB8, FromSlice}; - - #[derive(Debug, Serialize, Deserialize)] - pub struct Config { - pub color: RGB8, - pub wheel_color: Option, - } - - impl Config { - pub fn save(&self) -> Result<(), ConfyError> { - confy::store("deathadder", None, self) - } - - pub fn load() -> Option { - match confy::load("deathadder", None) { - Ok(cfg) => Some(cfg), - Err(_) => None - } - } - } - - impl Default for Config { - fn default() -> Self { - Self { - color: RGB8::new(0xAA, 0xAA, 0xAA), - wheel_color: None - } - } - } +pub mod cfg; +pub mod error; +pub mod device; - #[derive(Debug)] - pub enum ParseRGBError { - WrongLength(usize), - ParseHex(ParseIntError), - } - - impl fmt::Display for ParseRGBError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ParseRGBError::WrongLength(len) => - write!(f, "excluding pre/suffixes, \ - string can only be of length 3 or 6 ({} given)", len), - ParseRGBError::ParseHex(ref pie) => - write!(f, "{}", pie), - } - } - } - - impl error::Error for ParseRGBError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match *self { - ParseRGBError::WrongLength(_) => None, - ParseRGBError::ParseHex(ref pie) => Some(pie), - } - } - } - - impl From for ParseRGBError { - fn from(err: ParseIntError) -> ParseRGBError { - ParseRGBError::ParseHex(err) - } - } +pub mod common { + use std::{num::ParseIntError, thread, time::Duration}; + use rusb::{Context, DeviceHandle}; + use core::mem::{size_of, size_of_val, MaybeUninit}; + use rgb::{RGB8, FromSlice}; + use crate::error::{ParseRGBError, USBResult, USBError}; pub fn rgb_from_hex(input: &str) -> Result { let s = input @@ -94,68 +38,429 @@ pub mod core { } } } -} -pub mod v2 { - use std::{time::Duration}; - use rusb::{ - Context, UsbContext, - }; - use rgb::{ - RGB8 - }; - use crate::core::Config; + // tried also 1ms with varying results + static USB_RECEIVER_WAIT: Duration = Duration::from_millis(10); + static USB_TXFER_TIMEOUT: Duration = Duration::from_secs(1); + + // const RAZER_USB_REPORT_LEN: usize = 0x5A; - pub fn preview_color(color: RGB8, wheel_color: Option) -> Result { - _set_color(color, wheel_color, false) + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + pub enum LedState { + Off = 0x00, + On = 0x01, } - pub fn set_color(color: RGB8, wheel_color: Option) -> Result { - _set_color(color, wheel_color, true) + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + pub enum LedStorage { + NoStore = 0x00, + VarStore = 0x01, } - fn _set_color(color: RGB8, wheel_color: Option, save: bool) -> Result { - let vid = 0x1532; - let pid = 0x0084; - - let timeout = Duration::from_secs(1); + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + pub enum Led { + Zero = 0x00, + ScrollWheel = 0x01, + Battery = 0x03, + Logo = 0x04, + Backlight = 0x05, + Macro = 0x07, + Game = 0x08, + RedProfile = 0x0C, + GreenProfile = 0x0D, + BlueProfile = 0x0E, + RightSide = 0x10, + LeftSide = 0x11, + ArgbCh1 = 0x1A, + ArgbCh2 = 0x1B, + ArgbCh3 = 0x1C, + ArgbCh4 = 0x1D, + ArgbCh5 = 0x1E, + ArgbCh6 = 0x1F, + Charging = 0x20, + FastCharging = 0x21, + FullyCharged = 0x22 + } + + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + pub enum LedEffect { + None = 0x00, + Static = 0x01, + Breathing = 0x02, + Spectrum = 0x03, + Wave = 0x04, + Reactive = 0x05, + Starlight = 0x07, + CustomFrame = 0x08, + } + + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + enum CmdStatus { + Busy = 0x01, + Successful = 0x02, + Failure = 0x03, + Timeout = 0x04, + NotSupported = 0x05, + } + + impl TryFrom for CmdStatus { + type Error = u8; - // save regardless of USB result and fail silently - if save { - _ = Config {color, wheel_color}.save(); + fn try_from(byte: u8) -> Result { + match byte { + x if x == CmdStatus::Busy as u8 => Ok(CmdStatus::Busy), + x if x == CmdStatus::Successful as u8 => Ok(CmdStatus::Successful), + x if x == CmdStatus::Failure as u8 => Ok(CmdStatus::Failure), + x if x == CmdStatus::Timeout as u8 => Ok(CmdStatus::Timeout), + x if x == CmdStatus::NotSupported as u8 => Ok(CmdStatus::NotSupported), + _ => Err(byte), + } } - - match Context::new() { - Ok(context) => match context.open_device_with_vid_pid(vid, pid) { - Some(handle) => { - - let mut packet: Vec = vec![ - // the start (no idea what they are) - 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0b, 0x0f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, - - // wheel RGB (3B) | body RGB (3B) - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - - // the trailer (no idea what they are either) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00 - ]; - - packet.splice(16..19, color.iter()); - packet.splice(13..16, wheel_color.unwrap_or(color).iter()); - - match handle.write_control(0x21, 9, 0x300, 0, - &packet, timeout) { - Ok(len) => Ok(format!("written {} bytes", len)), - Err(e) => Err(format!("could not write ctrl transfer: {}", e)) - } + } + + #[repr(u8)] + #[derive(Debug, Copy, Clone)] + pub enum PollingRate { + Hz1000 = 0x01, + Hz500 = 0x02, + Hz250 = 0x04, + Hz125 = 0x08, + } + + impl TryFrom for PollingRate { + type Error = u8; + + fn try_from(flag: u8) -> Result { + match flag { + x if x == PollingRate::Hz1000 as u8 => Ok(PollingRate::Hz1000), + x if x == PollingRate::Hz500 as u8 => Ok(PollingRate::Hz500), + x if x == PollingRate::Hz250 as u8 => Ok(PollingRate::Hz250), + x if x == PollingRate::Hz125 as u8 => Ok(PollingRate::Hz125), + _ => Err(flag), + } + } + } + + #[repr(C, packed)] + #[derive(Debug, Copy, Clone)] + pub struct RazerReport { + status: u8, + pub(crate) transaction_id: u8, + remaining_packets: u16, // big endian + protocol_type: u8, // 0x0 + data_size: u8, + command_class: u8, + command_id: u8, + pub(crate) arguments: [u8; 80], + crc: u8, // xor'ed bytes of report + reserved: u8, // 0x0 + } + + impl Default for RazerReport { + fn default() -> Self { + unsafe { + // safe as 0 a valid bit-pattern for all fields + MaybeUninit::::zeroed().assume_init() + } + } + } + + impl RazerReport { + fn init(cmd_cls: u8, cmd_id: u8, data_size: u8) -> Self { + Self { + command_class: cmd_cls, + command_id: cmd_id, + data_size: data_size, + ..Default::default() + } + } + + fn new(cmd_cls: u8, cmd_id: u8, args: &[u8]) -> Self { + let mut r = Self { + command_class: cmd_cls, + command_id: cmd_id, + data_size: args.len() as u8, + ..Default::default() + }; + r.arguments[..args.len()].copy_from_slice(args); + r + } + + fn update_crc(&mut self) -> &mut Self { + let s = self.bytes(); + + self.crc = s[2..88].iter().fold(0, |crc, x| crc ^ x); + self + } + + /// Converts this struct to network byte order in-place + fn to_network_byte_order_mut(&mut self) -> &mut Self { + self.remaining_packets = + (self.remaining_packets & 0xff) << 8 | + (self.remaining_packets >> 8); + self + } + + /// Returns a copy of this struct in network byte order + fn to_network_byte_order(mut self) -> Self { + self.to_network_byte_order_mut(); + self + } + + /// Converts this struct to host byte order in-place + fn to_host_byte_order_mut(&mut self) -> &mut Self { + self.to_network_byte_order_mut() + } + + /// Returns a copy of this struct in host byte order + fn to_host_byte_order(mut self) -> Self { + self.to_host_byte_order_mut(); + self + } + + /// Struct as a slice; fast, zero-copy; host byte order + fn bytes(&self) -> &[u8] { + unsafe { + core::slice::from_raw_parts( + (self as *const Self) as *const u8, + size_of::(), + ) + } + } + + /// Initializes this struct from the given slice. No conversion to byte order. + fn from(buffer: &[u8]) -> Option { + let c_buf = buffer.as_ptr(); + let s = c_buf as *mut Self; + + if size_of::() == size_of_val(buffer) { + unsafe { + let ref s2 = *s; + Some(*s2) } - None => Err(format!("could not find device {:04x}:{:04x}", vid, pid)), - }, - Err(e) => Err(format!("could not initialize libusb: {}", e)), + } else { + None + } + } + + /// Converts to network byte order and returns a copy as_slice + fn pack(self) -> Vec { + self.to_network_byte_order().bytes().into() + } + + /// Converts to network byte order in-place(!) and returns as_slice. + /// Equivalent to self.to_network_byte_order_mut().bytes() + #[allow(dead_code)] + fn pack_mut(&mut self) -> &[u8] { + self.to_network_byte_order_mut().bytes() + } + + /// Construct from slice and return a copy in host byte order + fn unpack(buffer: &[u8]) -> Option { + match Self::from(buffer) { + Some(rep) => Some(rep.to_host_byte_order()), + None => None + } + } + + } + + fn razer_send_control_msg( + usb_dev: &DeviceHandle, + data: &RazerReport, + report_index: u16 + ) -> USBResult { + let request = 0x09u8; // HID_REQ_SET_REPORT + let request_type = 0x21u8; // USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT + let value = 0x300u16; + + let written = usb_dev.write_control( + request_type, request, value, report_index, + &data.pack(), USB_TXFER_TIMEOUT)?; + + // wait here otherwise we fail on any subsequent HID_REQ_GET_REPORTs + thread::sleep(USB_RECEIVER_WAIT); + + Ok(written) + } + + fn razer_get_usb_response( + usb_dev: &DeviceHandle, + report_index: u16, + request_report: &RazerReport, + response_index: u16 + ) -> USBResult { + let written = razer_send_control_msg( + usb_dev, request_report, report_index)?; + if written != size_of_val(request_report) { + return Err(USBError::IncompleteWrite( + size_of_val(request_report), written)); + } + + let request = 0x01u8; // HID_REQ_GET_REPORT + let request_type = 0xA1u8; // USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN + let value = 0x300u16; + let mut buffer = [0u8; size_of::()]; + let read = usb_dev.read_control( + request_type, request, value, response_index, + &mut buffer, USB_TXFER_TIMEOUT)?; + if read != size_of::() { + return Err(USBError::IncompleteRead( + size_of::(), read)); + } + + // RazerReport::from() won't fail with this buf + Ok(RazerReport::unpack(&buffer).unwrap()) + } + + fn razer_get_report( + usb_dev: &DeviceHandle, + request: &RazerReport + ) -> USBResult { + let index = 0u16; + razer_get_usb_response(usb_dev, index, request, index) + } + + pub(crate) fn razer_send_payload( + usb_dev: &DeviceHandle, + request: &mut RazerReport + ) -> USBResult { + request.update_crc(); + let response = razer_get_report(usb_dev, request)?; + + if response.remaining_packets != request.remaining_packets || + response.command_class != request.command_class || + response.command_id != request.command_id { + return Err(USBError::ResponseMismatch); + } + + match CmdStatus::try_from(response.status) { + Ok(CmdStatus::Busy) => Err(USBError::DeviceBusy), + Ok(CmdStatus::Failure) => Err(USBError::CommandFailed), + Ok(CmdStatus::NotSupported) => Err(USBError::CommandNotSupported), + Ok(CmdStatus::Timeout) => Err(USBError::CommandTimeout), + Ok(CmdStatus::Successful) => Ok(response), + Err(status) => Err(USBError::ResponseUnknownStatus(status)), } } -} \ No newline at end of file + + pub(crate) fn razer_chroma_standard_get_serial() -> RazerReport { + RazerReport::init(0x00, 0x82, 0x16) + } + + pub(crate) fn razer_chroma_misc_get_dpi_xy(variable_storage: LedStorage) -> RazerReport { + let mut report = RazerReport::init(0x04, 0x85, 0x07); + report.arguments[0] = variable_storage as u8; + report + } + + pub(crate) fn razer_chroma_misc_set_dpi_xy( + variable_storage: LedStorage, + dpi_x: u16, + dpi_y: u16 + ) -> RazerReport { + // Keep the DPI within bounds + let dpi_x = dpi_x.clamp(100, 30000); + let dpi_y = dpi_y.clamp(100, 30000); + RazerReport::new(0x04, 0x05, &[ + variable_storage as u8, + ((dpi_x >> 8) & 0xFF) as u8, + (dpi_x & 0xFF) as u8, + ((dpi_y >> 8) & 0xFF) as u8, + (dpi_y & 0xFF) as u8, + 0x00u8, + 0x00u8, + ]) + } + + pub(crate) fn razer_chroma_misc_get_polling_rate() -> RazerReport { + RazerReport::init(0x00, 0x85, 0x01) + } + + pub(crate) fn razer_chroma_misc_set_polling_rate(polling_rate: PollingRate) -> RazerReport { + RazerReport::new(0x00, 0x05, &[ + polling_rate as u8, + ]) + } + + pub(crate) fn razer_naga_trinity_effect_static( + variable_storage: LedStorage, + effect: LedEffect, + logo_rgb: RGB8, + scroll_rgb: RGB8, + ) -> RazerReport { + RazerReport::new(0x0f, 0x03, &[ + variable_storage as u8, + 0x00, // LED ID ? + 0x00, // Unknown + 0x00, // Unknown + effect as u8, + scroll_rgb.r, scroll_rgb.g, scroll_rgb.b, + logo_rgb.r, logo_rgb.g, logo_rgb.b, + ]) + } + + fn razer_chroma_extended_matrix_effect_base( + arg_size: u8, + variable_storage: LedStorage, + led: Led, + effect: LedEffect, + ) -> RazerReport { + let mut report = RazerReport::init(0x0f, 0x02, arg_size); + report.arguments[0] = variable_storage as u8; + report.arguments[1] = led as u8; + report.arguments[2] = effect as u8; + report + } + + #[allow(dead_code)] + pub(crate) fn razer_chroma_extended_matrix_effect_none( + variable_storage: LedStorage, + led: Led, + ) -> RazerReport { + razer_chroma_extended_matrix_effect_base( + 0x06, variable_storage, led, LedEffect::None) + } + + pub(crate) fn razer_chroma_extended_matrix_effect_static( + variable_storage: LedStorage, + led: Led, + rgb: RGB8, + ) -> RazerReport { + let mut report = razer_chroma_extended_matrix_effect_base( + 0x09, variable_storage, led, LedEffect::Static); + report.arguments[5] = 0x01; + report.arguments[6] = rgb.r; + report.arguments[7] = rgb.g; + report.arguments[8] = rgb.b; + report + } + + pub(crate) fn razer_chroma_extended_matrix_brightness( + variable_storage: LedStorage, + led: Led, + brightness: u8, + ) -> RazerReport { + RazerReport::new(0x0F, 0x04, &[ + variable_storage as u8, + led as u8, + (255.0 * brightness.clamp(0, 100) as f32 / 100.0).round() as u8, + ]) + } + + pub(crate) fn razer_chroma_extended_matrix_get_brightness( + variable_storage: LedStorage, + led: Led, + ) -> RazerReport { + RazerReport::new(0x0F, 0x84, &[ + variable_storage as u8, + led as u8, + 0x00, // brightness + ]) + } +} diff --git a/task-scheduler.xml b/task-scheduler.xml deleted file mode 100644 index c80c4a5..0000000 Binary files a/task-scheduler.xml and /dev/null differ