Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for extended adjustable dpi #2237

Merged
merged 3 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions lib/logitech_receiver/hidpp20.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,10 +1372,6 @@ def show(self):
_yaml.SafeLoader.add_constructor("!OnboardProfiles", OnboardProfiles.from_yaml)
_yaml.add_representer(OnboardProfiles, OnboardProfiles.to_yaml)

#
#
#


def feature_request(device, feature, function=0x00, *params, no_reply=False):
if device.online and device.features:
Expand Down
135 changes: 110 additions & 25 deletions lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ def build(cls, setting_class, device):

class DpiSlidingXY(_RawXYProcessing):
def activate_action(self):
self.dpiSetting = next(filter(lambda s: s.name == "dpi", self.device.settings), None)
self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
self.dpiChoices = list(self.dpiSetting.choices)
self.otherDpiIdx = self.device.persister.get("_dpi-sliding", -1) if self.device.persister else -1
if not isinstance(self.otherDpiIdx, int) or self.otherDpiIdx < 0 or self.otherDpiIdx >= len(self.dpiChoices):
Expand Down Expand Up @@ -801,7 +801,7 @@ def move_action(self, dx, dy):

class MouseGesturesXY(_RawXYProcessing):
def activate_action(self):
self.dpiSetting = next(filter(lambda s: s.name == "dpi", self.device.settings), None)
self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
self.fsmState = "idle"
self.initialize_data()

Expand Down Expand Up @@ -932,39 +932,47 @@ def build(cls, setting_class, device):
return validator


class AdjustableDpi(_Setting):
"""Pointer Speed feature"""
def produce_dpi_list(feature, function, ignore, device, direction):
dpi_bytes = b""
for i in range(0, 0x100): # there will be only a very few iterations performed
reply = device.feature_request(feature, function, 0x00, direction, i)
assert reply, "Oops, DPI list cannot be retrieved!"
dpi_bytes += reply[ignore:]
if dpi_bytes[-2:] == b"\x00\x00":
break
dpi_list = []
i = 0
while i < len(dpi_bytes):
val = _bytes2int(dpi_bytes[i : i + 2])
if val == 0:
break
if val >> 13 == 0b111:
step = val & 0x1FFF
last = _bytes2int(dpi_bytes[i + 2 : i + 4])
assert len(dpi_list) > 0 and last > dpi_list[-1], f"Invalid DPI list item: {val!r}"
dpi_list += range(dpi_list[-1] + step, last + 1, step)
i += 4
else:
dpi_list.append(val)
i += 2
return dpi_list


# Assume sensorIdx 0 (there is only one sensor)
# [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB
# [3] setSensorDpi(sensorIdx, dpi)
class AdjustableDpi(_Setting):
name = "dpi"
label = _("Sensitivity (DPI)")
description = _("Mouse movement sensitivity")
feature = _F.ADJUSTABLE_DPI
rw_options = {"read_fnid": 0x20, "write_fnid": 0x30}
choices_universe = _NamedInts.range(200, 4000, str, 50)
choices_universe = _NamedInts.range(100, 4000, str, 50)

class validator_class(_ChoicesV):
@classmethod
def build(cls, setting_class, device):
# [1] getSensorDpiList(sensorIdx)
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
assert reply, "Oops, DPI list cannot be retrieved!"
dpi_list = []
step = None
for val in _unpack("!7H", reply[1 : 1 + 14]):
if val == 0:
break
if val >> 13 == 0b111:
assert step is None and len(dpi_list) == 1, f"Invalid DPI list item: {val!r}"
step = val & 0x1FFF
else:
dpi_list.append(val)
if step:
assert len(dpi_list) == 2, f"Invalid DPI list range: {dpi_list!r}"
dpi_list = range(dpi_list[0], dpi_list[1] + 1, step)
return cls(choices=_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None
dpilist = produce_dpi_list(setting_class.feature, 0x10, 1, device, 0)
setting = cls(choices=_NamedInts.list(dpilist), byte_count=2, write_prefix_bytes=b"\x00") if dpilist else None
setting.dpilist = dpilist
return setting

def validate_read(self, reply_bytes): # special validator to use default DPI if needed
reply_value = _bytes2int(reply_bytes[1:3])
Expand All @@ -975,6 +983,82 @@ def validate_read(self, reply_bytes): # special validator to use default DPI if
return valid_value


class ExtendedAdjustableDpi(_Setting):
# the extended version allows for two dimensions, longer dpi descriptions, but still assume only one sensor
name = "dpi_extended"
label = _("Sensitivity (DPI)")
description = _("Mouse movement sensitivity") + "\n" + _("May need Onboard Profiles set to Disable to be effective.")
feature = _F.EXTENDED_ADJUSTABLE_DPI
rw_options = {"read_fnid": 0x50, "write_fnid": 0x60}
keys_universe = _NamedInts(X=0, Y=1, LOD=2)
choices_universe = _NamedInts.range(100, 4000, str, 50)
choices_universe[0] = "LOW"
choices_universe[1] = "MEDIUM"
choices_universe[2] = "HIGH"
keys = _NamedInts(X=0, Y=1, LOD=2)

def write_key_value(self, key, value, save=True):
if isinstance(self._value, dict):
self._value[key] = value
else:
self._value = {key: value}
result = self.write(self._value, save)
return result[key] if isinstance(result, dict) else result

class validator_class(_ChoicesMapV):
@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(setting_class.feature, 0x10, 0x00)
y = bool(reply[2] & 0x01)
lod = bool(reply[2] & 0x02)
choices_map = {}
dpilist_x = produce_dpi_list(setting_class.feature, 0x20, 3, device, 0)
choices_map[setting_class.keys["X"]] = _NamedInts.list(dpilist_x)
if y:
dpilist_y = produce_dpi_list(setting_class.feature, 0x20, 3, device, 1)
choices_map[setting_class.keys["Y"]] = _NamedInts.list(dpilist_y)
if lod:
choices_map[setting_class.keys["LOD"]] = _NamedInts(LOW=0, MEDIUM=1, HIGH=2)
validator = cls(choices_map=choices_map, byte_count=2, write_prefix_bytes=b"\x00")
validator.y = y
validator.lod = lod
validator.keys = setting_class.keys
return validator

def validate_read(self, reply_bytes): # special validator to read entire setting
dpi_x = _bytes2int(reply_bytes[3:5]) if reply_bytes[1:3] == 0 else _bytes2int(reply_bytes[1:3])
assert dpi_x in self.choices[0], f"{self.__class__.__name__}: failed to validate dpi_x value {dpi_x:04X}"
value = {self.keys["X"]: dpi_x}
if self.y:
dpi_y = _bytes2int(reply_bytes[7:9]) if reply_bytes[5:7] == 0 else _bytes2int(reply_bytes[5:7])
assert dpi_y in self.choices[1], f"{self.__class__.__name__}: failed to validate dpi_y value {dpi_y:04X}"
value[self.keys["Y"]] = dpi_y
if self.lod:
lod = reply_bytes[9]
assert lod in self.choices[2], f"{self.__class__.__name__}: failed to validate lod value {lod:02X}"
value[self.keys["LOD"]] = lod
return value

def prepare_write(self, new_value, current_value=None): # special preparer to write entire setting
data_bytes = self._write_prefix_bytes
if new_value[self.keys["X"]] not in self.choices[self.keys["X"]]:
raise ValueError(f"invalid value {new_value!r}")
data_bytes += _int2bytes(new_value[0], 2)
if self.y:
if new_value[self.keys["Y"]] not in self.choices[self.keys["Y"]]:
raise ValueError(f"invalid value {new_value!r}")
data_bytes += _int2bytes(new_value[self.keys["Y"]], 2)
else:
data_bytes += b"\x00\x00"
if self.lod:
if new_value[self.keys["LOD"]] not in self.choices[self.keys["LOD"]]:
raise ValueError(f"invalid value {new_value!r}")
data_bytes += _int2bytes(new_value[self.keys["LOD"]], 1)
else:
data_bytes += b"\x00"
return data_bytes


class SpeedChange(_Setting):
"""Implements the ability to switch Sensitivity by clicking on the DPI_Change button."""

Expand Down Expand Up @@ -1632,6 +1716,7 @@ def build(cls, setting_class, device):
ExtendedReportRate,
PointerSpeed, # simple
AdjustableDpi, # working
ExtendedAdjustableDpi,
SpeedChange,
# Backlight, # not working - disabled temporarily
Backlight2, # working
Expand Down
76 changes: 50 additions & 26 deletions tests/logitech_receiver/test_setting_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,42 +383,24 @@ class FeatureTest:
Setup(
FeatureTest(settings_templates.AdjustableDpi, 800, 400, version=0x03),
common.NamedInts.list([400, 800, 1600]),
hidpp.Response("000190032006400000000000000000", 0x0410),
hidpp.Response("000190032006400000", 0x0410, "000000"),
hidpp.Response("000320", 0x0420),
hidpp.Response("000190", 0x0430, "000190"),
),
Setup(
FeatureTest(settings_templates.AdjustableDpi, 1600, 400, version=0x03),
common.NamedInts.list([400, 800, 1600]),
hidpp.Response("000190032006400000000000000000", 0x0410),
hidpp.Response("0000000640", 0x0420),
hidpp.Response("000190", 0x0430, "000190"),
FeatureTest(settings_templates.AdjustableDpi, 256, 512, version=0x03),
common.NamedInts.list([256, 512]),
hidpp.Response("000100e10002000000", 0x0410, "000000"),
hidpp.Response("000100", 0x0420),
hidpp.Response("000200", 0x0430, "000200"),
),
Setup(
FeatureTest(settings_templates.AdjustableDpi, 400, 800, version=0x03),
common.NamedInts.list([400, 800, 1200, 1600]),
hidpp.Response("000190E19006400000000000000000", 0x0410),
hidpp.Response("000190E19006400000000000000000", 0x0410, "000000"),
hidpp.Response("000190", 0x0420),
hidpp.Response("000320", 0x0430, "000320"),
),
# Setup(
# FeatureTest(settings_templates.ExtendedAdjustableDpi, 256, 512, version=0x09),
# common.NamedInts.list([256, 512]),
# hidpp.Response("000000", 0x0910, "00"), # no y direction
# hidpp.Response("0000000100e10002000000", 0x0920, "000000"),
# hidpp.Response("000100", 0x0950),
# hidpp.Response("000200", 0x0960, "000200"),
# ),
# Setup(
# FeatureTest(settings_templates.ExtendedAdjustableDpi, 0x64, 0x164, version=0x09),
# common.NamedInts.list([0x064, 0x074, 0x084, 0x0A4, 0x0C4, 0x0E4, 0x0124, 0x0164, 0x01C4]),
# hidpp.Response("000001", 0x0910, "00"), # supports y direction
# hidpp.Response("0000000064E0100084E02000C4E02000", 0x0920, "000000"),
# hidpp.Response("000001E4E0400124E0400164E06001C4", 0x0920, "000001"),
# hidpp.Response("00000000000000000000000000000000", 0x0920, "000002"),
# hidpp.Response("000064", 0x0950),
# hidpp.Response("0001640164", 0x0960, "0001640164"),
# ),
Setup(
FeatureTest(settings_templates.Multiplatform, 0, 1),
common.NamedInts(**{"MacOS 0.1-0.5": 0, "iOS 0.1-0.7": 1, "Linux 0.2-0.9": 2, "Windows 0.3-0.9": 3}),
Expand Down Expand Up @@ -613,6 +595,48 @@ def test_simple_template(test, mocker, mock_gethostname):
hidpp.Response("02FF0000", 0x0410, "02FF0000"), # write one value
hidpp.Response("00", 0x0470, "00"), # finish
),
Setup(
FeatureTest(settings_templates.ExtendedAdjustableDpi, {0: 256}, {0: 512}, 2, offset=0x9),
{common.NamedInt(0, "X"): common.NamedInts.list([256, 512])},
hidpp.Response("000000", 0x0910, "00"), # no y direction, no lod
hidpp.Response("0000000100e10002000000", 0x0920, "000000"),
hidpp.Response("00010000000000000000", 0x0950),
hidpp.Response("000100000000", 0x0960, "000100000000"),
hidpp.Response("000200000000", 0x0960, "000200000000"),
),
Setup(
FeatureTest(settings_templates.ExtendedAdjustableDpi, {0: 0x64, 1: 0xE4}, {0: 0x164}, 2, offset=0x9),
{
common.NamedInt(0, "X"): common.NamedInts.list([0x064, 0x074, 0x084, 0x0A4, 0x0C4, 0x0E4, 0x0124, 0x0164, 0x01C4]),
common.NamedInt(1, "Y"): common.NamedInts.list([0x064, 0x074, 0x084, 0x0A4, 0x0C4, 0x0E4, 0x0124, 0x0164]),
},
hidpp.Response("000001", 0x0910, "00"), # supports y direction, no lod
hidpp.Response("0000000064E0100084E02000C4E02000", 0x0920, "000000"),
hidpp.Response("000001E4E0400124E0400164E06001C4", 0x0920, "000001"),
hidpp.Response("00000000000000000000000000000000", 0x0920, "000002"),
hidpp.Response("0000000064E0100084E02000C4E02000", 0x0920, "000100"),
hidpp.Response("000001E4E0400124E040016400000000", 0x0920, "000101"),
hidpp.Response("000064007400E4007400", 0x0950),
hidpp.Response("00006400E400", 0x0960, "00006400E400"),
hidpp.Response("00016400E400", 0x0960, "00016400E400"),
),
Setup(
FeatureTest(settings_templates.ExtendedAdjustableDpi, {0: 0x64, 1: 0xE4, 2: 1}, {1: 0x164}, 2, offset=0x9),
{
common.NamedInt(0, "X"): common.NamedInts.list([0x064, 0x074, 0x084, 0x0A4, 0x0C4, 0x0E4, 0x0124, 0x0164, 0x01C4]),
common.NamedInt(1, "Y"): common.NamedInts.list([0x064, 0x074, 0x084, 0x0A4, 0x0C4, 0x0E4, 0x0124, 0x0164]),
common.NamedInt(2, "LOD"): common.NamedInts(LOW=0, MEDIUM=1, HIGH=2),
},
hidpp.Response("000003", 0x0910, "00"), # supports y direction and lod
hidpp.Response("0000000064E0100084E02000C4E02000", 0x0920, "000000"),
hidpp.Response("000001E4E0400124E0400164E06001C4", 0x0920, "000001"),
hidpp.Response("00000000000000000000000000000000", 0x0920, "000002"),
hidpp.Response("0000000064E0100084E02000C4E02000", 0x0920, "000100"),
hidpp.Response("000001E4E0400124E040016400000000", 0x0920, "000101"),
hidpp.Response("000064007400E4007401", 0x0950),
hidpp.Response("00006400E401", 0x0960, "00006400E401"),
hidpp.Response("000064016401", 0x0960, "000064016401"),
),
]


Expand All @@ -623,7 +647,7 @@ def test_key_template(test, mocker):
spy_request = mocker.spy(device, "request")

setting = settings_templates.check_feature(device, tst.sclass)
assert setting
assert setting is not None
if isinstance(setting, list):
setting = setting[0]
if isinstance(test.choices, dict):
Expand Down
Loading