diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index e6b08151..2481a3e4 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -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) diff --git a/lib/logitech_receiver/hidpp20_constants.py b/lib/logitech_receiver/hidpp20_constants.py index e5c7f85d..fd7cc69c 100644 --- a/lib/logitech_receiver/hidpp20_constants.py +++ b/lib/logitech_receiver/hidpp20_constants.py @@ -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 diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 302fb188..9b3b7fe6 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -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,