settings: add scroll ratchet force setting

This commit is contained in:
Peter F. Patel-Schneider 2025-10-30 06:01:51 -04:00
parent b96d0bbe0b
commit cff0110f81
3 changed files with 41 additions and 4 deletions

Binary file not shown.

View File

@ -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, # 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), # 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). # write_prefix_bytes is a byte string to write before the value (default empty).
# RangeValidator is for an integer in a range. It takes # 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). # 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. # 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: # 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} 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) # 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) # 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) # 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 HiresSmoothResolution, # working
HiresMode, # simple HiresMode, # simple
ScrollRatchet, # simple ScrollRatchet, # simple
ScrollRatchetTorque,
SmartShift, # working SmartShift, # working
ScrollRatchetEnhanced, ScrollRatchetEnhanced,
SmartShiftEnhanced, # simple SmartShiftEnhanced, # simple

View File

@ -531,12 +531,13 @@ class RangeValidator(Validator):
kwargs["max_value"] = setting_class.max_value kwargs["max_value"] = setting_class.max_value
return cls(**kwargs) 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 assert max_value > min_value
self.min_value = min_value self.min_value = min_value
self.max_value = max_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.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)) self._byte_count = math.ceil(math.log(max_value + 1, 256))
if byte_count: if byte_count:
assert self._byte_count <= byte_count assert self._byte_count <= byte_count
@ -544,7 +545,7 @@ class RangeValidator(Validator):
assert self._byte_count < 8 assert self._byte_count < 8
def validate_read(self, reply_bytes): 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.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}" assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return reply_value return reply_value
@ -553,7 +554,7 @@ class RangeValidator(Validator):
if new_value < self.min_value or new_value > self.max_value: if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid choice {new_value!r}") raise ValueError(f"invalid choice {new_value!r}")
current_value = self.validate_read(current_value) if current_value is not None else None 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 # 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 return None if current_value is not None and current_value == new_value else to_write