From 9c768d60a1c7d349a6f396afb72441af79c7d337 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Mon, 14 Mar 2016 00:03:00 +0100 Subject: [PATCH] Add full support for adjustable DPI Feature 0x2201 as used by the MX Master. Valid DPI values are read directly from the device. Based on Logitech specifications. --- lib/logitech_receiver/common.py | 5 ++ lib/logitech_receiver/descriptors.py | 7 +-- lib/logitech_receiver/settings.py | 9 ++- lib/logitech_receiver/settings_templates.py | 63 ++++++++++++++++++--- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/logitech_receiver/common.py b/lib/logitech_receiver/common.py index 1a70fa93..8e3124ec 100644 --- a/lib/logitech_receiver/common.py +++ b/lib/logitech_receiver/common.py @@ -115,6 +115,11 @@ class NamedInts(object): # assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed) self._fallback = None + @classmethod + def list(cls, items, name_generator=lambda x: str(x)): + values = {name_generator(x): x for x in items} + return NamedInts(**values) + @classmethod def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1): values = {name_generator(x): x for x in range(from_value, to_value + 1, step)} diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index a8d5cc75..8d08e8e3 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -90,7 +90,6 @@ def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, # _PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) -_MX_MASTER_DPIS = _NamedInts.range(400, 1600, step=200) # # @@ -267,11 +266,7 @@ _D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A', ], ) -_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041', - settings=[ - _FS.dpi(choices=_MX_MASTER_DPIS), - ], - ) +_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041') _D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002', registers=(_R.battery_status, ), diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index bd16f6f5..aaaf64aa 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -317,7 +317,11 @@ class ChoicesValidator(object): kind = KIND.choice - def __init__(self, choices): + """Translates between NamedInts and a byte sequence. + :param choices: a list of NamedInts + :param bytes_count: the size of the derived byte sequence. If None, it + will be calculated from the choices.""" + def __init__(self, choices, bytes_count=None): assert choices is not None assert isinstance(choices, _NamedInts) assert len(choices) > 2 @@ -326,6 +330,9 @@ class ChoicesValidator(object): max_bits = max(x.bit_length() for x in choices) self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0) + if bytes_count: + assert self._bytes_count <= bytes_count + self._bytes_count = bytes_count assert self._bytes_count < 8 def validate_read(self, reply_bytes): diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 5e20f5a5..e7f1d052 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -23,6 +23,10 @@ from __future__ import absolute_import, division, print_function, unicode_litera from .i18n import _ from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 +from .common import ( + bytes2int as _bytes2int, + NamedInts as _NamedInts, + ) from .settings import ( KIND as _KIND, Setting as _Setting, @@ -71,14 +75,28 @@ def feature_toggle(name, feature, return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind) def feature_choices(name, feature, choices, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, - kind=_KIND.choice, + read_function_id, write_function_id, + bytes_count=None, label=None, description=None, device_kind=None): assert choices - validator = _ChoicesV(choices) + validator = _ChoicesV(choices, bytes_count=bytes_count) rw = _FeatureRW(feature, read_function_id, write_function_id) - return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind) + return _Setting(name, rw, validator, kind=_KIND.choice, label=label, description=description, device_kind=device_kind) + +def feature_choices_dynamic(name, feature, choices_callback, + read_function_id, write_function_id, + bytes_count=None, + label=None, description=None, device_kind=None): + # Proxy that obtains choices dynamically from a device + def instantiate(device): + # Obtain choices for this feature + choices = choices_callback(device) + setting = feature_choices(name, feature, choices, + read_function_id, write_function_id, + bytes_count=bytes_count, + label=None, description=None, device_kind=None) + return setting(device) + return instantiate # # common strings for settings @@ -145,12 +163,40 @@ def _feature_smooth_scroll(): label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2], device_kind=_DK.mouse) -def _feature_adjustable_dpi(register=_R.mouse_dpi, choices=None): +def _feature_adjustable_dpi_choices(device): + # [1] getSensorDpiList(sensorIdx) + reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) + # Should not happen, but might happen when the user unplugs device while the + # query is being executed. TODO retry logic? + assert reply, 'Oops, DPI list cannot be retrieved!' + dpi_list = [] + step = None + for offset in range(0, 14, 2): + val = _bytes2int(reply[offset:offset+2]) + if val == 0: + break + if val >> 13 == 0b111: + assert offset == 2, 'Invalid DPI list item: %r' % val + step = val & 0x1fff + else: + dpi_list.append(val) + if step: + assert dpi_list == 2, 'Invalid DPI list range: %r' % dpi_list + dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) + # getSensorDpi/setSensorDpi use (sensorIdx, dpiMSB, dpiLSB). Assume for now + # that sensorIdx is always zero and represent dpi 400 as 0x000190. + dpi_vals_list = [dpi << 8 for dpi in dpi_list] + return _NamedInts.list(dpi_vals_list, name_generator=lambda x: str(x >> 8)) + +def _feature_adjustable_dpi(): """Pointer Speed feature""" - return feature_choices(_DPI[0], _F.ADJUSTABLE_DPI, choices, - # TODO: is this really the read function? + # [2] getSensorDpi(sensorIdx) + # [3] setSensorDpi(sensorIdx, dpi) + return feature_choices_dynamic(_DPI[0], _F.ADJUSTABLE_DPI, + _feature_adjustable_dpi_choices, read_function_id=0x20, write_function_id=0x30, + bytes_count=3, label=_DPI[1], description=_DPI[2], device_kind=_DK.mouse) @@ -223,3 +269,4 @@ def check_feature_settings(device, already_known): check_feature(_SMOOTH_SCROLL[0], _F.HI_RES_SCROLLING) check_feature(_FN_SWAP[0], _F.FN_INVERSION) check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap') + check_feature(_DPI[0], _F.ADJUSTABLE_DPI)