diff --git a/docs/devices/MX Master 4.text b/docs/devices/MX Master 4.text new file mode 100644 index 00000000..aedfc34a Binary files /dev/null and b/docs/devices/MX Master 4.text differ diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 9c8c4d6d..9f1616e0 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -99,8 +99,11 @@ class State(enum.Enum): # mask is used to keep only some bits from a sequence of bits, this can be an integer or a byte string, # read_skip_byte_count is the number of bytes to ignore at the beginning of the read value (default 0), # write_prefix_bytes is a byte string to write before the value (default empty). + # RangeValidator is for an integer in a range. It takes # byte_count is number of bytes that the value is stored in (defaults to size of max_value). +# read_skip_byte_count is as for BooleanV +# write_prefix_bytes is as for BooleanV # RangeValidator uses min_value and max_value from the setting class as minimum and maximum. # ChoicesValidator is for symbolic choices. It takes one positional and three keyword arguments: @@ -698,6 +701,38 @@ class ScrollRatchetEnhanced(ScrollRatchet): rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} +class ScrollRatchetTorque(settings.Setting): + name = "scroll-ratchet-torque" + label = _("Scroll Wheel Ratchet Torque") + description = _("Change the torque needed to overcome the ratchet.") + feature = _F.SMART_SHIFT_ENHANCED + min_value = 1 + max_value = 100 + rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} + + class rw_class(settings.FeatureRW): + def write(self, device, data_bytes): + ratchetSetting = next(filter(lambda s: s.name == "scroll-ratchet", device.settings), None) + if ratchetSetting: # for MX Master 4, the ratchet setting needs to be written for changes to take effect + ratchet_value = ratchetSetting.read(True) + data_bytes = ratchet_value.to_bytes(1, "big") + data_bytes[1:] + result = super().write(device, data_bytes) + return result + + class validator_class(settings_validator.RangeValidator): + @classmethod + def build(cls, setting_class, device): + reply = device.feature_request(_F.SMART_SHIFT_ENHANCED, 0x00) + if reply[0] & 0x01: # device supports tunable torque + return cls( + min_value=setting_class.min_value, + max_value=setting_class.max_value, + byte_count=1, + write_prefix_bytes=b"\x00\x00", # don't change mode or disengage, but see above + read_skip_byte_count=2, + ) + + # the keys for the choice map are Logitech controls (from special_keys) # each choice value is a NamedInt with the string from a task (to be shown to the user) # and the integer being the control number for that task (to be written to the device) @@ -1756,6 +1791,7 @@ SETTINGS: list[settings.Setting] = [ HiresSmoothResolution, # working HiresMode, # simple ScrollRatchet, # simple + ScrollRatchetTorque, SmartShift, # working ScrollRatchetEnhanced, SmartShiftEnhanced, # simple diff --git a/lib/logitech_receiver/settings_validator.py b/lib/logitech_receiver/settings_validator.py index cd241d4d..ddc164ff 100644 --- a/lib/logitech_receiver/settings_validator.py +++ b/lib/logitech_receiver/settings_validator.py @@ -531,12 +531,13 @@ class RangeValidator(Validator): kwargs["max_value"] = setting_class.max_value return cls(**kwargs) - def __init__(self, min_value=0, max_value=255, byte_count=1): + def __init__(self, min_value=0, max_value=255, byte_count=1, read_skip_byte_count=0, write_prefix_bytes=b""): assert max_value > min_value self.min_value = min_value self.max_value = max_value + self.read_skip_byte_count = read_skip_byte_count + self.write_prefix_bytes = write_prefix_bytes self.needs_current_value = True # read and check before write (needed for ADC power and probably a good idea anyway) - self._byte_count = math.ceil(math.log(max_value + 1, 256)) if byte_count: assert self._byte_count <= byte_count @@ -544,7 +545,7 @@ class RangeValidator(Validator): assert self._byte_count < 8 def validate_read(self, reply_bytes): - reply_value = common.bytes2int(reply_bytes[: self._byte_count]) + reply_value = common.bytes2int(reply_bytes[self.read_skip_byte_count : self.read_skip_byte_count + self._byte_count]) assert reply_value >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" return reply_value @@ -553,7 +554,7 @@ class RangeValidator(Validator): if new_value < self.min_value or new_value > self.max_value: raise ValueError(f"invalid choice {new_value!r}") current_value = self.validate_read(current_value) if current_value is not None else None - to_write = common.int2bytes(new_value, self._byte_count) + to_write = self.write_prefix_bytes + common.int2bytes(new_value, self._byte_count) # current value is known and same as value to be written return None to signal not to write it return None if current_value is not None and current_value == new_value else to_write