feat: PRO X 2 Superstrike support with click haptics, actuation point and rapid trigger config support

This commit is contained in:
Caio Quirino 2026-02-11 15:31:02 +01:00
parent 42e0e391b5
commit d7584c3790
No known key found for this signature in database
3 changed files with 164 additions and 0 deletions

View File

@ -406,6 +406,7 @@ _D("MX Vertical Wireless Mouse", codename="MX Vertical", protocol=4.5, wpid="407
_D("Wireless Mouse Pebble M350", codename="Pebble", protocol=2.0, wpid="4080")
_D("MX Master 3 Wireless Mouse", codename="MX Master 3", protocol=4.5, wpid="4082", btid=0xB023)
_D("PRO X Wireless", kind="mouse", codename="PRO X", wpid="4093", usbid=0xC094)
_D("PRO X 2 Superstrike", kind="mouse", codename="PRO X 2 Superstrike", wpid="40BD")
_D("G9 Laser Mouse", codename="G9", usbid=0xC048, interface=1, protocol=1.0)
_D("G9x Laser Mouse", codename="G9x", usbid=0xC066, interface=1, protocol=1.0)

View File

@ -74,6 +74,7 @@ class SupportedFeature(IntEnum):
REPROG_CONTROLS_V2_2 = 0x1B02 # LogiOptions 2.10.73 features.xml
REPROG_CONTROLS_V3 = 0x1B03
REPROG_CONTROLS_V4 = 0x1B04
SUPERSTRIKE_TUNING = 0x1B0C # PRO X 2 Superstrike HITS tuning (actuation, rapid trigger, haptics)
REPORT_HID_USAGE = 0x1BC0
PERSISTENT_REMAPPABLE_ACTION = 0x1C00
WIRELESS_DEVICE_STATUS = 0x1D4B

View File

@ -1809,6 +1809,167 @@ class ForceSensing(settings_new.Settings):
return setting
# PRO X 2 Superstrike HITS (Hall-Effect Inductive Trigger Switch) settings
class _SuperstrikeActuationRW(settings.FeatureRW):
"""RW for Superstrike actuation point (1-10) per button."""
def __init__(self, feature, button_index, max_actuation):
super().__init__(feature, read_fnid=0x20, write_fnid=0x10) # Write is 0x10, not 0x30
self.button_index = button_index
self.max_actuation = max_actuation # Device's max value (e.g., 40)
def read(self, device, data_bytes=b""):
res = device.feature_request(self.feature, 0x20, self.button_index)
if not res:
return b"\x05" # default mid-point
# Actuation is in byte 1, scale from device range (1-max) to UI range (1-10)
device_val = res[1]
ui_val = max(1, min(10, round(device_val * 10 / self.max_actuation)))
return bytes([ui_val])
def write(self, device, data_bytes):
current = device.feature_request(self.feature, 0x20, self.button_index)
if not current:
return None
# Scale from UI range (1-10) to device range (4-max, step 4 based on observed behavior)
ui_val = data_bytes[0]
device_val = max(4, min(self.max_actuation, round(ui_val * self.max_actuation / 10)))
# Round to nearest multiple of 4 (device seems to quantize)
device_val = ((device_val + 2) // 4) * 4
# Write using func 0x10: [button_index, actuation, rt, byte3]
return device.feature_request(self.feature, 0x10, self.button_index, device_val, current[2], current[3])
class _SuperstrikeRapidTriggerLevelRW(settings.FeatureRW):
"""RW for Superstrike rapid trigger sensitivity (1=fast to 5=slow) per button."""
def __init__(self, feature, button_index, max_rt_level):
super().__init__(feature, read_fnid=0x20, write_fnid=0x10) # Write is 0x10
self.button_index = button_index
self.max_rt_level = max_rt_level # Device's max RT level (e.g., 20)
def read(self, device, data_bytes=b""):
res = device.feature_request(self.feature, 0x20, self.button_index)
if not res or res[2] == 0:
return b"\x03" # default mid-point if RT disabled
# Scale from device range (1-max) to UI range (1-5)
device_val = res[2]
ui_val = max(1, min(5, round(device_val * 5 / self.max_rt_level)))
return bytes([ui_val])
def write(self, device, data_bytes):
current = device.feature_request(self.feature, 0x20, self.button_index)
if not current:
return None
# Scale from UI range (1-5) to device range (1-max)
ui_val = data_bytes[0]
device_val = max(1, min(self.max_rt_level, round(ui_val * self.max_rt_level / 5)))
return device.feature_request(self.feature, 0x10, self.button_index, current[1], device_val, current[3])
class _SuperstrikeHapticsRW(settings.FeatureRW):
"""RW for Superstrike click haptics (0-5) per button via TUNING feature byte3.
Device stores haptics in byte3 of the TUNING response with values 0, 4, 8, 12, 16, 20.
UI range is 0-5 where 0=off and 5=strongest.
"""
def __init__(self, feature, button_index, max_haptics):
super().__init__(feature, read_fnid=0x20, write_fnid=0x10)
self.button_index = button_index
self.max_haptics = max_haptics # Device's max haptics value (e.g., 20)
def read(self, device, data_bytes=b""):
res = device.feature_request(self.feature, 0x20, self.button_index)
if not res:
return b"\x03" # default mid-point
# Haptics is in byte 3, scale from device range (0-max) to UI range (0-5)
device_val = res[3]
ui_val = round(device_val * 5 / self.max_haptics)
return bytes([ui_val])
def write(self, device, data_bytes):
current = device.feature_request(self.feature, 0x20, self.button_index)
if not current:
return None
# Scale from UI range (0-5) to device range (0-max, step 4)
ui_val = data_bytes[0]
device_val = round(ui_val * self.max_haptics / 5)
# Round to nearest multiple of 4 (device quantizes to 0, 4, 8, 12, 16, 20)
device_val = ((device_val + 2) // 4) * 4
device_val = min(device_val, self.max_haptics)
# Write: [button_index, actuation, rt, haptics]
return device.feature_request(self.feature, 0x10, self.button_index, current[1], current[2], device_val)
class SuperstrikeTuning_(settings.Setting):
"""HITS tuning settings: actuation point, rapid trigger, and haptics configuration."""
name = "superstrike-tuning_"
label = _("HITS Tuning")
description = _("Configure Hall-Effect Inductive Trigger Switch settings including haptics.")
feature = _F.SUPERSTRIKE_TUNING
@classmethod
def build(cls, device):
if cls.feature not in device.features:
return None
# Get capabilities: [flags, button_count, max_actuation, max_rt, max_haptics, ...]
caps = device.feature_request(cls.feature, 0x00)
if not caps or len(caps) < 5:
return None
button_count = min(caps[1], 2) # Byte 1 is button count, limit to 2 (left/right)
max_actuation = caps[2] if caps[2] > 0 else 40 # Byte 2 is max actuation
max_rt_level = caps[3] if caps[3] > 0 else 20 # Byte 3 is max RT level
max_haptics = caps[4] if caps[4] > 0 else 20 # Byte 4 is max haptics
if button_count == 0:
return None
button_names = [_("Left Button"), _("Right Button")]
all_settings = []
# Group settings by feature type for better UI organization
# Actuation Points (UI: 1-10, device: 1-max_actuation)
for i in range(button_count):
btn_name = button_names[i] if i < len(button_names) else f"Button {i}"
rw_act = _SuperstrikeActuationRW(cls.feature, i, max_actuation)
val_act = settings_validator.RangeValidator(min_value=1, max_value=10)
s_act = settings.Setting(device, rw_act, val_act)
s_act.name = f"superstrike-tuning_actuation-{i}"
s_act.label = f"{btn_name} Actuation Point"
s_act.description = _("Actuation point depth (1=shallow, 10=deep).")
all_settings.append(s_act)
# Rapid Trigger Levels (UI: 1-5, device: 1-max_rt_level)
# Note: Device doesn't support disabling rapid trigger (rt_level=0 returns error)
for i in range(button_count):
btn_name = button_names[i] if i < len(button_names) else f"Button {i}"
rw_rt_lvl = _SuperstrikeRapidTriggerLevelRW(cls.feature, i, max_rt_level)
val_rt_lvl = settings_validator.RangeValidator(min_value=1, max_value=5)
s_rt_lvl = settings.Setting(device, rw_rt_lvl, val_rt_lvl)
s_rt_lvl.name = f"superstrike-tuning_rapid-trigger-level-{i}"
s_rt_lvl.label = f"{btn_name} Rapid Trigger Level"
s_rt_lvl.description = _("Rapid trigger sensitivity (1=fast, 5=slow).")
all_settings.append(s_rt_lvl)
# Click Haptics (UI: 0-5, device: 0-max_haptics in steps of 4)
for i in range(button_count):
btn_name = button_names[i] if i < len(button_names) else f"Button {i}"
rw_haptics = _SuperstrikeHapticsRW(cls.feature, i, max_haptics)
val_haptics = settings_validator.RangeValidator(min_value=0, max_value=5)
s_haptics = settings.Setting(device, rw_haptics, val_haptics)
s_haptics.name = f"superstrike-tuning_haptics-{i}"
s_haptics.label = f"{btn_name} Click Haptics"
s_haptics.description = _("Click haptic feedback intensity (0=off, 5=strongest).")
all_settings.append(s_haptics)
return all_settings if all_settings else None
class HapticLevel(settings.Setting):
name = "haptic-level"
label = _("Haptic Feeback Level")
@ -1932,6 +2093,7 @@ SETTINGS: list[settings.Setting] = [
Gesture2Gestures, # working
Gesture2Divert,
Gesture2Params, # working
SuperstrikeTuning_,
HapticLevel,
PlayHapticWaveForm,
Sidetone,