Skip to content

feat(Hardware Support): Razer Tartarus Pro#518

Draft
dripsnek wants to merge 8 commits intoShadowBlip:mainfrom
dripsnek:razer_tartarus_pro
Draft

feat(Hardware Support): Razer Tartarus Pro#518
dripsnek wants to merge 8 commits intoShadowBlip:mainfrom
dripsnek:razer_tartarus_pro

Conversation

@dripsnek
Copy link
Copy Markdown

@dripsnek dripsnek commented Feb 11, 2026

This WIP PR implements support for the Razer Tartarus Pro gaming keypad. To date I've not found anything which allows usage of the analog mode specific to this model under Linux. This aims to bridge that gap.

The need for a driver exists because analog mode generates vendor defined HID reports that contain the positional value of each key instead of events. These must be processed and mapped to input events and this seemed like a natural fit for InputPlumber to handle.

I think this device is the first of its kind for InputPlumber and assuming the project wishes to extend support to these peripherals, how to represent them from a profile perspective in my mind is the major piece of work to go.

The driver has two modes, basic and analog. Basic mode simply uses the Tartarus Pro in its power-up configuration as a regular keyboard/mouse hybrid and is configured as any other device. Analog mode requires commanding the Tartarus Pro. This can occur manually before starting InputPlumber or if the OpenRazer kernel module >3.12.0 is loaded the driver will use it to set the mode when InputPlumber starts.

This implementation takes full control of the hidraw (x3) and evdev nodes (x4) but passes through the last endpoint allowing lighting control software e.g. OpenRGB or Polychromatic to manage the RGB functions whilst InputPlumber is running.

The analog event handler is implemented using simple linear regressions on a small buffer* to drive a per-key state machine and supports the following:

  • User defined actuation points between 1.5mm** and 3.6mm depths
  • Dual-function keys
  • Retrigger*** with shared or separate upward and downward thresholds
  • Wooting-style continuous retrigger***

* The buffer depth of 5 was based on protocol analysis of fast key transitions from the device
** Quantization and implementation quirks label the minimum at 1.4mm but the Tartarus Pro marketing spec stands.
*** AKA Rapid Trigger from other vendors

Analog mode usage:

Analog mode parameters currently reside at 50-razer_tartarus_pro.yaml in the analogkeys mapping. There are 5 arrays of 20 values, each value left-to-right represents the numbered key on the Tartarus Pro in order, putting aside 0 indexing. All 20 keys have individual analog settings and these will apply to any loaded key binding profiles. Values are sanitised with a specific exception in place for zeroes. Analog parameters cannot be changed at runtime, a restart of InputPlumber is required to take effect.

Config Definition Limits Notes
primary_actuation Absolute depth value for actuation of first key bind 1.4 to 3.6mm in 0.1mm steps Cannot be disabled. A value of 0 will map to effectively 1.4mm. Key bindings are per basic mode, can be rebound using a profile
secondary_actuation Absolute depth value for actuation of second key bind 1.4 to 3.6mm in 0.1mm steps Must be greater than primary_actuation to enable. Can be explicitly disabled by setting to 0, otherwise any invalid setting will be interpreted as disabled. Prevents usage of retrigger though a similar effect exists for the second actuation point due to how events are processed. Secondary bindings are mapped to fixed "phantom" keys which can be further customized via profile, see secondary_rebind.yaml
retrigger_reset_threshold Travel distance in mm to reset a key. 0 to 2.1mm in 0.1mm steps Automatically disabled when secondary_actuation is enabled. 0mm disables the function
retrigger_trigger_threshold Travel distance in mm which triggers a key after being reset. 0 to 2.1mm in 0.1mm steps If this value is 0 then retrigger_reset_threshold will be used for this purpose. This variable exists in case separate reset and trigger thresholds are desired
continuous_retrigger Continue retrigger logic until key reaches the top of travel boolean If false (default) retrigger logic is disabled if the actuation point is crossed during keyup

Limits to this PR:

  • No plans to include other types of analog events to this driver considering the recent issues surrounding Snap Tap.
  • No Hypershift or equivalent mode, I think that's more of a wider InputPlumber capability rather than something specific to a device driver.
  • The profile LEDs on the side of the Tartarus should eventually be supported by OpenRazer and changing their state is better managed by a profile front-end application rather than InputPlumber directly.
  • RGB support is already implemented by Polychromatic and OpenRGB.

Other topics influencing implementation, not necessarily impacting the path out of WIP:

  • Device details:
    Like a lot of products in this class it is possible to acquire the serial number, firmware revision and other such data from the device registers. This could be used to uniquely assign profiles to a specific device or implement quirks for a given firmware. Should this be incorporated?
  • Multi-instance support:
    The Tartarus Pro is a left-handed device but someone, somewhere will somehow find a way to use 2 of them. Should we embrace this or defend against it?

Assuming this type of device can be included, I propose the following to navigate out of WIP:

  • Decide to keep basic mode or refactor to only manage analog mode
  • Decide to keep num_enum crate or refactor impacted code (event.rs / driver.rs - handle_basic)
  • If keeping basic mode, feat: add mouse wheel support #514 merged to support scroll wheel remapping
  • Incorporate device profile support and remove hard coded settings
  • Return device to basic mode either on demand or if InputPlumber closes
  • Write up device usage .md file at nominated location
  • Wire up analog event handler to the driver event vector
  • Test what happens when two devices are connected
  • Housekeeping prior to merge

Copy link
Copy Markdown
Contributor

@pastaq pastaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for taking so long to review. Overall I am impressed with the direction this is going. I do have a few concerns about a couple of the approaches, but we can discuss them below in each of the issues I raised.

}
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, we try to stay away from sending input reports back to the device aside from rumble events in DInput. I would much rather see these sorts of things added to a kernel driver, exposed over sysfs, and controlled by udev, either through rules or the udev crate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenRazer just added support for the Tartarus Pro so there is no need to have a custom control pane now. Code has (pending push) been adjusted to use those sysfs nodes, but that does introduce a dependency on the razerkbd module for analog mode and potentially profile filtering to specific boards.

let _ = Self::transaction(packet, &device).unwrap();
}

let mut zeroes = VecDeque::with_capacity(REGRESSION_WINDOW);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining the purpose of this array?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commentary in pending push.

// 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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we use a PackedStruct instead of manually decoding the reports.

See https://github.com/ShadowBlip/InputPlumber/blob/main/src/drivers/legos/hid_report.rs for an example.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section removed as control related code offloaded to razerkbd module

Ok(events)
}

fn handle_analog(&mut self, keys: &[u8]) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general this entire function is difficult to follow. It is very long, the variable names aren't obvious for what they are attempting to accomplish, and the comments refer to actions that aren't well defined. If you could take another look at this to make it less ambiguous I'd be able to provide more constructive feedback.

Please add a plain English explanation above the function using documentation markers ///

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored in next push

Ok(Vec::new())
}

fn linear_regression(&self, data: &[f64]) -> Option<f64> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a plain English explanation above the function using documentation markers ///

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

// 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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a plain English explanation above the function using documentation markers ///

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section removed as control related code offloaded to razerkbd module

// 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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a plain English explanation above the function using documentation markers ///

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

InputValue::Bool(event.pressed),
),
razer_tartarus_pro::event::KeyCodes::ScrollDown => {
NativeEvent::new(Capability::Mouse(Mouse::Wheel), InputValue::Float(-1.0))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always -1 from the hardware? Many scroll wheels can have larger values depending on if they are high resolution or moved quickly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USB packet captures and evtest at the raw device show it only ever returning 1 and -1, tried scrolling at multiple speeds.

razer_tartarus_pro::event::KeyCodes::Blank => {
NativeEvent::new(Capability::NotImplemented, InputValue::None)
}
razer_tartarus_pro::event::KeyCodes::ScrollUp => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please order these events logically to make finding a given event easier in the match list. I.e. scroll wheels together, either numerically or alphabetically for the keys, etc.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered

}

/// List of all capabilities that the Razer Tartarus Pro implements
pub const CAPABILITIES: &[Capability] = &[
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort these as well please.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered

@dripsnek dripsnek force-pushed the razer_tartarus_pro branch from 18a2cee to 7dc77a8 Compare March 21, 2026 06:16
@dripsnek dripsnek requested a review from pastaq March 21, 2026 07:08
@dripsnek dripsnek force-pushed the razer_tartarus_pro branch from 7dc77a8 to dad1e02 Compare April 4, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants