diff --git a/Cargo.lock b/Cargo.lock index 3a96a516..f0f01077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,6 +1062,7 @@ dependencies = [ "log", "mio", "nix 0.30.1", + "num_enum", "packed_struct", "procfs", "rand 0.9.2", @@ -1449,6 +1450,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "num_threads" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 0abfbc9a..df7bacd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ log = { version = "0.4.29", features = [ ] } mio = { version = "1.1.1", features = ["os-poll", "os-ext", "net"] } nix = { version = "0.30.1", features = ["fs"] } +num_enum = "0.7.5" packed_struct = "0.10.1" procfs = "0.18.0" rand = "0.9.2" diff --git a/rootfs/usr/share/inputplumber/devices/50-razer_tartarus_pro.yaml b/rootfs/usr/share/inputplumber/devices/50-razer_tartarus_pro.yaml new file mode 100644 index 00000000..b5cc91cc --- /dev/null +++ b/rootfs/usr/share/inputplumber/devices/50-razer_tartarus_pro.yaml @@ -0,0 +1,69 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +# Schema version number +version: 1 + +# The type of configuration schema +kind: CompositeDevice + +# Name of the composite device mapping +name: Razer Tartarus Pro + +# Only use this profile if *any* of the given matches matches. If this list is +# empty,then the source devices will *always* be checked. +# /sys/class/dmi/id/product_name +matches: [] + +# Maximum number of source devices per CompositeDevice. +maximum_sources: 7 + +# One or more source devices to combine into a single virtual device. The events +# from these devices will be watched and translated according to the key map. +source_devices: + - group: keyboard + blocked: true + unique: false + evdev: + name: "Razer Razer Tartarus Pro*" + handler: event* + - group: keyboard + unique: false + udev: + attributes: + - name: idVendor + value: "1532" + - name: idProduct + value: "0244" + - name: bInterfaceNumber + value: "00" + subsystem: hidraw + - group: keyboard + unique: false + udev: + attributes: + - name: idVendor + value: "1532" + - name: idProduct + value: "0244" + - name: bInterfaceNumber + value: "01" + subsystem: hidraw + - group: mouse + unique: false + passthrough: true + udev: + attributes: + - name: idVendor + value: "1532" + - name: idProduct + value: "0244" + - name: bInterfaceNumber + value: "02" + subsystem: hidraw + +# The target input device(s) to emulate by default +target_devices: + - mouse + - keyboard + +options: + auto_manage: true \ No newline at end of file diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs index 7d40ee0c..90ac9bb9 100644 --- a/src/drivers/mod.rs +++ b/src/drivers/mod.rs @@ -9,6 +9,7 @@ pub mod legos; pub mod msi_claw; pub mod opineo; pub mod oxp_tty; +pub mod razer_tartarus_pro; pub mod rog_ally; pub mod steam_deck; pub mod unified_gamepad; diff --git a/src/drivers/razer_tartarus_pro/driver.rs b/src/drivers/razer_tartarus_pro/driver.rs new file mode 100644 index 00000000..e41da66c --- /dev/null +++ b/src/drivers/razer_tartarus_pro/driver.rs @@ -0,0 +1,463 @@ +use super::event::{Event, KeyCodes}; +use crate::udev::device::UdevDevice; +use hidapi::HidDevice; +use std::collections::VecDeque; +use std::{error::Error, ffi::CString}; + +pub const VID: u16 = 0x1532; +pub const PID: u16 = 0x0244; + +const RAZER_HID_ARRAY_SIZE: usize = 91; +const MESSAGE_DATA_PAYLOAD: usize = 24; +const RAZER_FEATURE_ENDPOINT: i32 = 2; +const TREND_KEYDOWN: f64 = 1.0; +const TREND_KEYUP: f64 = -1.0; +const REGRESSION_WINDOW: usize = 5; +const KEY_COUNT: usize = 20; +const UNIT_TO_MM: f64 = 0.1 / 12.0; + +pub struct Driver { + device: HidDevice, + razer_message_id: u8, + hysteresis: VecDeque<[u8; KEY_COUNT]>, + key_actions: [AnalogAction; KEY_COUNT], + key_state: Vec, +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct AnalogAction { + actuation_point: (u8, u8), + retrigger_window: (f64, f64), // as mm + crt_en: bool, + actuated: (bool, bool), + retrigger_track: f64, // as mm + retrigger_go: bool, + keydown_up_n: bool, // Reflects the previous trend on a key +} + +// This driver implementation is shared across all three HID handles on the +// Tartarus Pro. Certain code-paths will only execute on certain handles. +// Refer to handle_input_report() + +impl Driver { + pub fn new(udevice: UdevDevice) -> Result> { + let hidrawpath = udevice.devnode(); + let cs_path = CString::new(hidrawpath.clone())?; + let api = hidapi::HidApi::new()?; + let device = api.open_path(&cs_path)?; + let info = device.get_device_info()?; + let mut razer_message_id = 0 as u8; + if info.vendor_id() != VID || info.product_id() != PID { + return Err(format!("Device '{hidrawpath}' is not a Razer Tartarus Pro").into()); + } + if info.interface_number() == RAZER_FEATURE_ENDPOINT { + // Check if we can get serial number + let mut packet = Self::razer_report(0x0, 0x82, 0x16, &[0x0], &mut razer_message_id); + let output = Self::transaction(packet, &device).unwrap(); + log::info!( + "Tartarus Serial Number: {:?}", + String::from_utf8(output).unwrap().trim_end_matches("\0") + ); + // Enable analog mode + packet = Self::razer_report(0x0, 0x4, 0x2, &[0x3, 0x0], &mut razer_message_id); + log::info!("Enabling Tartarus Pro analog mode"); + let _ = Self::transaction(packet, &device).unwrap(); + } + + let mut zeroes = VecDeque::with_capacity(REGRESSION_WINDOW); + for _ in 0..zeroes.capacity() { + zeroes.push_back([0; KEY_COUNT]); + } + let mut check = Self { + device, + razer_message_id, + hysteresis: zeroes, + key_actions: [AnalogAction::default(); KEY_COUNT], + key_state: Vec::new(), + }; + check.key_actions[0].actuation_point = (0x24, 0x78); + check.key_actions[0].retrigger_window = (0.0, 0.0); + check.key_actions[0].crt_en = false; + + Ok(check) + } + + pub fn poll(&mut self) -> Result, Box> { + let mut buf = [0; MESSAGE_DATA_PAYLOAD]; + let bytes_read = self.device.read(&mut buf[..])?; + let slice = &buf[..bytes_read]; + let events = self.handle_input_report(slice, bytes_read)?; + Ok(events) + } + + // The hidapi crate enforces the Windows-ism of needing a dud zeroth byte for sending feature + // reports to devices with no report ID specified. For unused report ID This means our window + // is [1..] and we ignore [0], though leave it 0. The XOR is always the penultimate byte with + // the ultimate byte being 0. Byte [1] is a status byte which for PC->DEV is 0. Replies + // always start with 0x2. Bytes [3..5] are used in other Razer devices but not the Tartarus + // so we leave them 0. + fn razer_report( + cmd_class: i32, + cmd_id: i32, + data_size: i32, + payload: &[u8], + id: &mut u8, + ) -> [u8; RAZER_HID_ARRAY_SIZE] { + let mut message: [u8; RAZER_HID_ARRAY_SIZE] = [0; RAZER_HID_ARRAY_SIZE]; + message[1] = 0; // Always 0 when sending to the device + message[2] = *id as u8; + message[6] = data_size as u8; + message[7] = cmd_class as u8; + message[8] = cmd_id as u8; + message[9..(9 + payload.len())].copy_from_slice(&payload); + + // Generate checksum + let mut xorval = 0; + for refr in message[6..89].iter() { + xorval ^= refr; + } + + message[89] = xorval as u8; + + // Increment next transaction id + *id = id.wrapping_add(1); + return message; + } + + // This validates any received reports against the feature request and provides the payload + // as a vector for further processing. + fn razer_decapsulate( + request: &[u8], + response: &[u8], + ) -> Result, Box> { + let mut is_valid = true; + is_valid = (response[1] == 2) && is_valid; + is_valid = response[3..5].iter().all(|&b| b == 0) && is_valid; + is_valid = (response[6] <= 0x50) && is_valid; + is_valid = (response[7] == request[7]) && is_valid; + is_valid = (response[8] == request[8]) && is_valid; + + // Validate the checksum + let mut xorval = 0; + for refr in response[6..89].iter() { + xorval ^= refr; + } + is_valid = (response[89] == xorval) && is_valid; + if !is_valid { + log::error!("Invalid Tartarus transaction received"); + Ok(Vec::new()) + } else { + Ok(response[9..(9 + response[6] as usize)].to_vec()) + } + } + + fn transaction( + packet: [u8; RAZER_HID_ARRAY_SIZE], + handle: &HidDevice, + ) -> Result, Box> { + if handle.get_device_info().unwrap().interface_number() == RAZER_FEATURE_ENDPOINT { + let _ = handle.send_feature_report(&packet); + let mut feature_buffer: [u8; RAZER_HID_ARRAY_SIZE] = [0; RAZER_HID_ARRAY_SIZE]; + let _ = handle.get_feature_report(&mut feature_buffer); + return Ok(Self::razer_decapsulate(&packet, &feature_buffer)?); + } + log::error!("Tartarus attempting to transact on incorrect endpoint"); + return Ok(Vec::new()); + } + + fn handle_input_report( + &mut self, + buf: &[u8], + bytes_read: usize, + ) -> Result, Box> { + let info = self.device.get_device_info()?; + let mut events = Vec::new(); + + // Depending on what handle we actually are (this code runs on all 3) + // we have different reports that we can expect and respond to in kind. + + // Endpoint 1.1 is always played as per its HID report descriptor but it is simple enough + // to re-implement here. There is no report ID, it gets straight to business. + // This interface captures the D-pad and the aux key. + + // Endpoint 1.2 has two personalities, if report ID 1 is seen then it is in default + // keyboard mode and is played exactly as its descriptor says. If report ID 6 is seen + // that is analog mode so we need to run the threshold checks. + // This interface regardless of personality captures the 20 numbered keys. + + // Endpoint 1.3 is in the same basket as 1.1, different patterns to match though. + // This interface captures the scroll wheel. + + match info.interface_number() { + 0 => self.handle_basic(buf, KeyCodes::Aux, false), + 1 => { + match buf[0] { + 0x1 => return self.handle_basic(buf, KeyCodes::Blank, true), + 0x6 => { + return self.handle_analog(&buf[1..21]); + } + _ => { + // Other report types exist but don't appear to be + // actually used. + Ok(events) + } + } + } + 2 => self.handle_basic(buf, KeyCodes::MClick, false), + _ => Ok(events), + } + } + + // The basic case for all 3 endpoints is essentially identical. The first byte is unique for + // each; For endpoints 1.1 and 1.3 this is where code 0x04 is interpreted and discarded for + // endpoint 1.2 as it was a report ID which has served its purpose if we got this far. + // Given the conversion to variant space it is open to misinterpretation if left there. + fn handle_basic( + &mut self, + buf: &[u8], + key_replace: KeyCodes, + overwrite: bool, + ) -> Result, Box> { + let mut events = Vec::new(); + let mut pad_state: Vec = buf.iter().map(|&s| KeyCodes::from(s)).collect(); + + // Override Byte 0 as specified and remove any blanks + if pad_state[0] == KeyCodes::KeyTwelve || overwrite { + pad_state[0] = key_replace; + } + pad_state.retain(|x| *x != KeyCodes::Blank); + + // If a key is present in the report then it was pressed + for i in &pad_state { + events.push(Event { + key: i.clone(), + pressed: true, + }); + } + + // If a key is missing compared to last time then indicate it is no longer pressed + for i in &self.key_state { + if !pad_state.contains(&i) { + events.push(Event { + key: i.clone(), + pressed: false, + }); + } + } + + // Save state for next time + self.key_state = pad_state; + Ok(events) + } + + fn handle_analog(&mut self, keys: &[u8]) -> Result, Box> { + let key_arr: &[u8; KEY_COUNT]; + if let Ok(value) = <&[u8] as TryInto<&[u8; KEY_COUNT]>>::try_into(keys) { + key_arr = value; + } else { + log::error!("Incorrect size array passed to handle_analog"); + return Ok(Vec::new()); + } + + self.hysteresis.pop_front(); + self.hysteresis.push_back(*key_arr); + let previous_state = self.hysteresis.get(self.hysteresis.len() - 2).unwrap(); + let front_state = self.hysteresis.front().unwrap(); + // Add Vector of events TBD + + // Manage per-key functions as each report gives us a snapshot of the whole matrix + for (index, element) in keys.iter().enumerate() { + // Short-circuit as it is unlikely all 20 keys are pressed. + // If a key is not actuated and has a value of 0, ignore processing this round. + if !(self.key_actions[index].actuated.0 ^ self.key_actions[index].actuated.1) + && *element == 0 + { + continue; + } + + // Establish the direction of travel for the keys + let run: Vec = self + .hysteresis + .iter() + .map(|arr| arr[index] as f64) + .collect(); + let trend = self.linear_regression(&run).unwrap_or_else(|| 0.0); + + // If we are not actuated, establish whether we should + if !(self.key_actions[index].actuated.0 ^ self.key_actions[index].actuated.1) { + // Exception for 1.5mm as any depression will trigger it + if self.key_actions[index].actuation_point.0 == 0 && *element > 0 { + self.key_actions[index].actuated.0 = true; + // Do keydown stuff (1.5mm case) + log::info!("A1 Key {} Keydown @ {}!", index, *element); + } else if trend >= TREND_KEYDOWN + && self.key_actions[index].actuation_point.0 <= *element + { + self.key_actions[index].actuated.0 = true; + // Do keydown stuff (standard) + log::info!("A1 Key {} Keydown @ {}!", index, *element); + } + } else { + // We are actuated, what happens now is based mainly on trends + + // Picking a trend criteria of ± 1 is important as it allows us to initialize + // the retrigger distance given all numbers are in the same direction. + // Using the keydown_up_n flag we can determine whether to look at only the + // previous value or look at the start of the buffer. + if trend >= TREND_KEYDOWN { + // Check if we need to perform dual function + if self.key_actions[index].actuation_point.1 + > self.key_actions[index].actuation_point.0 + && !self.key_actions[index].actuated.1 + && self.key_actions[index].actuation_point.1 <= *element + { + self.key_actions[index].actuated.1 = true; + self.key_actions[index].actuated.0 = false; + // Do keydown stuff (second function) + log::info!("A1 Key {} Keyup @ {}!", index, *element); + log::info!("A2 Key {} Keydown @ {}!", index, *element); + } + + // Manage retrigger + // retrigger_track uses negative and positive values. + // When negative we track reset, when positive we track triggering. + if self.key_actions[index].retrigger_go { + if self.key_actions[index].retrigger_track == 0.0 { + self.key_actions[index].retrigger_track = + *element as f64 - front_state[index] as f64; + self.key_actions[index].retrigger_track *= UNIT_TO_MM; + } else { + let difference = *element as f64 - previous_state[index] as f64; + self.key_actions[index].retrigger_track += difference * UNIT_TO_MM; + } + + // Retrigger windows can be shared or separate. + // Manage the case where keydown is separate from keyup + if self.key_actions[index].retrigger_window.1 != 0.0 + && self.key_actions[index].retrigger_track + >= self.key_actions[index].retrigger_window.1 + { + self.key_actions[index].retrigger_go = false; + self.key_actions[index].retrigger_track = 0.0; + // Do keydown stuff + // Then the shared case. + } else if self.key_actions[index].retrigger_window.0 != 0.0 + && self.key_actions[index].retrigger_track + >= self.key_actions[index].retrigger_window.0 + { + self.key_actions[index].retrigger_go = false; + self.key_actions[index].retrigger_track = 0.0; + log::info!("Key {} retriggered!", index); + // Do keydown stuff + } + // Cases for backing off retrigger, such as travelling back down. + // We don't cancel, but we do effectively undo progress. + } else if self.key_actions[index].retrigger_track != 0.0 { + let difference: f64; + if !self.key_actions[index].keydown_up_n { + difference = *element as f64 - front_state[index] as f64; + } else { + difference = *element as f64 - previous_state[index] as f64; + } + self.key_actions[index].retrigger_track += difference * UNIT_TO_MM; + + // Regardless if we totally undo retrigger progress, zero it rather than + // making it harder to perform next time. + if self.key_actions[index].retrigger_track > 0.0 { + self.key_actions[index].retrigger_track = 0.0; + } + } + self.key_actions[index].keydown_up_n = true; + } else if trend <= TREND_KEYUP { + // Manage actuation point + if self.key_actions[index].actuation_point.0 >= *element { + // If the key returns to the neutral position everything resets. + if *element == 0 { + self.key_actions[index].retrigger_go = false; + self.key_actions[index].retrigger_track = 0.0; + if self.key_actions[index].actuated.0 { + self.key_actions[index].actuated = (false, false); + // Do keyup stuff + log::info!("Key {} Neutral! {:?}", index, run); + continue; + } + // If crt_en isn't covering for retrigger then stop when our + // actuation point has been met. + } else if !self.key_actions[index].crt_en { + self.key_actions[index].retrigger_go = false; + self.key_actions[index].retrigger_track = 0.0; + self.key_actions[index].actuated = (false, false); + // Do keyup stuff + log::info!("A1 Key {} Keyup @ {}!", index, *element); + continue; + } + } + + if self.key_actions[index].actuation_point.1 + > self.key_actions[index].actuation_point.0 + && self.key_actions[index].actuation_point.1 >= *element + && self.key_actions[index].actuated.1 + { + if !self.key_actions[index].crt_en { + self.key_actions[index].retrigger_go = false; + self.key_actions[index].retrigger_track = 0.0; + self.key_actions[index].actuated.1 = false; + // Do keyup stuff + log::info!("A2 Key {} Keyup @ {}!", index, *element); + continue; + } + } + // Manage retrigger + // The tuple representing the retrigger window is conditionally set. + // If the tracker is 0, then initialise the counter from where we are. + if !self.key_actions[index].retrigger_go + && self.key_actions[index].retrigger_window.0 != 0.0 + { + let difference: f64; + if self.key_actions[index].keydown_up_n { + difference = front_state[index] as f64 - *element as f64; + } else { + difference = previous_state[index] as f64 - *element as f64; + } + self.key_actions[index].retrigger_track -= difference * UNIT_TO_MM; + + if self.key_actions[index].retrigger_window.0 + <= self.key_actions[index].retrigger_track.abs() + { + log::info!("Key {} retrigger armed!", index); + self.key_actions[index].retrigger_track = 0.0; + self.key_actions[index].retrigger_go = true; + // Do keyup stuff + } + } + self.key_actions[index].keydown_up_n = false; + } + } + } + + Ok(Vec::new()) + } + + fn linear_regression(&self, data: &[f64]) -> Option { + // Implement simple linear regression equation + // m = (n*Σxy - ΣxΣy) / (n*Σx² - (Σx)²) + // where n >= 2 (defined by length of data) + + let n = data.len() as f64; + if n < 2.0 { + return None; + } + let sum_x: f64 = (0..data.len()).map(|i| i as f64).sum(); + let sum_y: f64 = data.iter().sum(); + let sum_xy: f64 = data.iter().enumerate().map(|(i, &y)| i as f64 * y).sum(); + let sum_xx: f64 = (0..data.len()).map(|i| (i as f64).powi(2)).sum(); + + let numerator = n * sum_xy - sum_x * sum_y; + let denominator = n * sum_xx - sum_x.powi(2); + + if denominator == 0.0 { + return Some(0.0); + } + Some(numerator / denominator) + } +} diff --git a/src/drivers/razer_tartarus_pro/event.rs b/src/drivers/razer_tartarus_pro/event.rs new file mode 100644 index 00000000..48ff71cb --- /dev/null +++ b/src/drivers/razer_tartarus_pro/event.rs @@ -0,0 +1,52 @@ +use num_enum::FromPrimitive; + +// The Tartarus Pro despite looking like a lot of things is ultimately +// just a set of buttons. + +#[derive(Clone, Debug)] +pub struct Event { + pub key: KeyCodes, + pub pressed: bool, +} + +// Complete set of key codes that a Tartarus Pro emits in standard mode +// Despite the varied interfaces, there is only one overloaded scancode (0x04) +// and fortunately it is in a fixed position in the report types and unique per +// endpoint so we only have do deal with one instance at a time. +#[derive(Clone, Debug, FromPrimitive, PartialEq)] +#[repr(u8)] +pub enum KeyCodes { + #[num_enum(default)] + Blank, + ScrollUp, + KeyTwelve = 0x04, // A / Aux / Middle Mouse click + KeyNineteen = 0x06, // C + KeyFourteen, // D + KeyNine, // E + KeyFifteen, // F + KeySeven = 0x14, // Q + KeyTen, // R + KeyThirteen, // S + KeyEight = 0x1A, // W + KeyEighteen, // X + KeySeventeen = 0x1D, // Z + KeyOne, + KeyTwo, + KeyThree, + KeyFour, + KeyFive, + KeySix = 0x2B, // Tab + KeyTwenty, // Spacebar + KeyEleven = 0x39, // Capslock + Right = 0x4F, + Left, + Down, + Up, + KeySixteen = 0xE1, // LShift + Aux = 0xFD, // Internal label to account for overload. Never sent by HW + MClick, // Internal label to account for overload. Never sent by HW + ScrollDown, +} + +// Analog mode is positional - key 1 is array index 1 so translating a report +// into variant space is trivial. diff --git a/src/drivers/razer_tartarus_pro/mod.rs b/src/drivers/razer_tartarus_pro/mod.rs new file mode 100644 index 00000000..a6a3c132 --- /dev/null +++ b/src/drivers/razer_tartarus_pro/mod.rs @@ -0,0 +1,2 @@ +pub mod driver; +pub mod event; diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 0358a375..6be30321 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -11,6 +11,7 @@ pub mod legos_touchpad; pub mod legos_xinput; pub mod msi_claw; pub mod opineo; +pub mod razer_tartarus_pro; pub mod rog_ally; pub mod steam_deck; pub mod xpad_uhid; @@ -26,6 +27,7 @@ use horipad_steam::HoripadSteam; use legos_imu::LegionSImuController; use legos_touchpad::LegionSTouchpadController; use msi_claw::MsiClaw; +use razer_tartarus_pro::RazerTartarusPro; use rog_ally::RogAlly; use xpad_uhid::XpadUhid; use zotac_zone::ZotacZone; @@ -62,6 +64,7 @@ enum DriverType { LegionGoXInput, MsiClaw, OrangePiNeo, + RazerTartarusPro, RogAlly, SteamDeck, Unknown, @@ -85,6 +88,7 @@ pub enum HidRawDevice { LegionGo(SourceDriver), MsiClaw(SourceDriver), OrangePiNeo(SourceDriver), + RazerTartarusPro(SourceDriver), RogAlly(SourceDriver), SteamDeck(SourceDriver), Vader4Pro(SourceDriver), @@ -107,6 +111,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.info_ref(), HidRawDevice::MsiClaw(source_driver) => source_driver.info_ref(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.info_ref(), + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.info_ref(), HidRawDevice::RogAlly(source_driver) => source_driver.info_ref(), HidRawDevice::SteamDeck(source_driver) => source_driver.info_ref(), HidRawDevice::Vader4Pro(source_driver) => source_driver.info_ref(), @@ -129,6 +134,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.get_id(), HidRawDevice::MsiClaw(source_driver) => source_driver.get_id(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_id(), + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.get_id(), HidRawDevice::RogAlly(source_driver) => source_driver.get_id(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_id(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_id(), @@ -151,6 +157,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.client(), HidRawDevice::MsiClaw(source_driver) => source_driver.client(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.client(), + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.client(), HidRawDevice::RogAlly(source_driver) => source_driver.client(), HidRawDevice::SteamDeck(source_driver) => source_driver.client(), HidRawDevice::Vader4Pro(source_driver) => source_driver.client(), @@ -173,6 +180,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.run().await, HidRawDevice::MsiClaw(source_driver) => source_driver.run().await, HidRawDevice::OrangePiNeo(source_driver) => source_driver.run().await, + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.run().await, HidRawDevice::RogAlly(source_driver) => source_driver.run().await, HidRawDevice::SteamDeck(source_driver) => source_driver.run().await, HidRawDevice::Vader4Pro(source_driver) => source_driver.run().await, @@ -195,6 +203,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.get_capabilities(), HidRawDevice::MsiClaw(source_driver) => source_driver.get_capabilities(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_capabilities(), + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.get_capabilities(), HidRawDevice::RogAlly(source_driver) => source_driver.get_capabilities(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_capabilities(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_capabilities(), @@ -225,6 +234,9 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::MsiClaw(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_output_capabilities(), + HidRawDevice::RazerTartarusPro(source_driver) => { + source_driver.get_output_capabilities() + } HidRawDevice::RogAlly(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_output_capabilities(), @@ -247,6 +259,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGo(source_driver) => source_driver.get_device_path(), HidRawDevice::MsiClaw(source_driver) => source_driver.get_device_path(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_device_path(), + HidRawDevice::RazerTartarusPro(source_driver) => source_driver.get_device_path(), HidRawDevice::RogAlly(source_driver) => source_driver.get_device_path(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_device_path(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_device_path(), @@ -390,6 +403,21 @@ impl HidRawDevice { SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::XpadUhid(source_device)) } + DriverType::RazerTartarusPro => { + let device = RazerTartarusPro::new(device_info.clone())?; + let options = SourceDriverOptions { + poll_rate: Duration::from_micros(500), + buffer_size: 1024, + }; + let source_device = SourceDriver::new_with_options( + composite_device, + device, + device_info.into(), + options, + conf, + ); + Ok(Self::RazerTartarusPro(source_device)) + } DriverType::RogAlly => { let device = RogAlly::new(device_info.clone())?; let options = SourceDriverOptions { @@ -536,6 +564,14 @@ impl HidRawDevice { return DriverType::Fts3528Touchscreen; } + // Razer Tartarus Pro + if vid == drivers::razer_tartarus_pro::driver::VID + && pid == drivers::razer_tartarus_pro::driver::PID + { + log::info!("Detected Razer Tartarus Pro"); + return DriverType::RazerTartarusPro; + } + // Rog Ally if vid == drivers::rog_ally::driver::VID && drivers::rog_ally::driver::PIDS.contains(&pid) { log::info!("Detected ROG Ally"); diff --git a/src/input/source/hidraw/razer_tartarus_pro.rs b/src/input/source/hidraw/razer_tartarus_pro.rs new file mode 100644 index 00000000..cb74c09b --- /dev/null +++ b/src/input/source/hidraw/razer_tartarus_pro.rs @@ -0,0 +1,205 @@ +use crate::drivers::razer_tartarus_pro::{self, driver::Driver}; +use crate::input::capability::{Capability, Keyboard, Mouse, MouseButton}; +use crate::input::event::{native::NativeEvent, value::InputValue}; +use crate::input::source::{InputError, OutputError, SourceInputDevice, SourceOutputDevice}; +use crate::udev::device::UdevDevice; + +use std::{error::Error, fmt::Debug}; + +/// RazerTartarusPro source device implementation +pub struct RazerTartarusPro { + driver: Driver, +} + +impl RazerTartarusPro { + /// Create a new source device with the given udev + /// device information + pub fn new(device_info: UdevDevice) -> Result> { + let driver = Driver::new(device_info)?; + Ok(Self { driver }) + } +} + +impl SourceOutputDevice for RazerTartarusPro {} + +impl SourceInputDevice for RazerTartarusPro { + /// Poll the given input device for input events + fn poll(&mut self) -> Result, InputError> { + let events = match self.driver.poll() { + Ok(events) => events, + Err(err) => { + log::error!("Got error polling!: {err:?}"); + return Err(err.into()); + } + }; + let native_events = translate_events(events); + Ok(native_events) + } + + /// Returns the possible input events this device is capable of emitting + fn get_capabilities(&self) -> Result, InputError> { + Ok(CAPABILITIES.into()) + } +} + +impl Debug for RazerTartarusPro { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RazerTartarusPro").finish() + } +} + +/// Translate the given events into native events +fn translate_events(events: Vec) -> Vec { + events.into_iter().map(translate_event).collect() +} + +fn translate_event(event: razer_tartarus_pro::event::Event) -> NativeEvent { + match event.key { + razer_tartarus_pro::event::KeyCodes::Blank => { + NativeEvent::new(Capability::NotImplemented, InputValue::None) + } + razer_tartarus_pro::event::KeyCodes::ScrollUp => { + NativeEvent::new(Capability::Mouse(Mouse::Wheel), InputValue::Float(1.0)) + } + razer_tartarus_pro::event::KeyCodes::KeySixteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyLeftShift), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyTwelve => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyA), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyNineteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyC), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyFourteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyD), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyNine => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyE), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyFifteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyF), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeySeven => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyQ), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyTen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyR), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyThirteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyS), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyEight => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyW), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyEighteen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyX), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeySeventeen => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyZ), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyOne => NativeEvent::new( + Capability::Keyboard(Keyboard::Key1), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyTwo => NativeEvent::new( + Capability::Keyboard(Keyboard::Key2), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyThree => NativeEvent::new( + Capability::Keyboard(Keyboard::Key3), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyFour => NativeEvent::new( + Capability::Keyboard(Keyboard::Key4), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyFive => NativeEvent::new( + Capability::Keyboard(Keyboard::Key5), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeySix => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyTab), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyTwenty => NativeEvent::new( + Capability::Keyboard(Keyboard::KeySpace), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::KeyEleven => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyCapslock), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::Right => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyRight), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::Left => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyLeft), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::Down => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyDown), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::Up => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyUp), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::Aux => NativeEvent::new( + Capability::Keyboard(Keyboard::KeyLeftAlt), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::MClick => NativeEvent::new( + Capability::Mouse(Mouse::Button(MouseButton::Middle)), + InputValue::Bool(event.pressed), + ), + razer_tartarus_pro::event::KeyCodes::ScrollDown => { + NativeEvent::new(Capability::Mouse(Mouse::Wheel), InputValue::Float(-1.0)) + } + } +} + +/// List of all capabilities that the Razer Tartarus Pro implements +pub const CAPABILITIES: &[Capability] = &[ + Capability::Keyboard(Keyboard::Key1), + Capability::Keyboard(Keyboard::Key2), + Capability::Keyboard(Keyboard::Key3), + Capability::Keyboard(Keyboard::Key4), + Capability::Keyboard(Keyboard::Key5), + Capability::Keyboard(Keyboard::KeyQ), + Capability::Keyboard(Keyboard::KeyW), + Capability::Keyboard(Keyboard::KeyE), + Capability::Keyboard(Keyboard::KeyR), + Capability::Keyboard(Keyboard::KeyT), + Capability::Keyboard(Keyboard::KeyA), + Capability::Keyboard(Keyboard::KeyS), + Capability::Keyboard(Keyboard::KeyD), + Capability::Keyboard(Keyboard::KeyF), + Capability::Keyboard(Keyboard::KeyLeftShift), + Capability::Keyboard(Keyboard::KeyZ), + Capability::Keyboard(Keyboard::KeyX), + Capability::Keyboard(Keyboard::KeyC), + Capability::Keyboard(Keyboard::KeyLeftAlt), + Capability::Keyboard(Keyboard::KeySpace), + Capability::Keyboard(Keyboard::KeyCapslock), + Capability::Keyboard(Keyboard::KeyUp), + Capability::Keyboard(Keyboard::KeyLeft), + Capability::Keyboard(Keyboard::KeyRight), + Capability::Keyboard(Keyboard::KeyDown), + Capability::Mouse(Mouse::Button(MouseButton::WheelUp)), + Capability::Mouse(Mouse::Button(MouseButton::WheelDown)), + Capability::Mouse(Mouse::Button(MouseButton::Middle)), + Capability::NotImplemented, +];