From 73344cbf26bc2a97bf76c248914e5391cfaff730 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Sun, 31 May 2015 00:27:23 +0200 Subject: [PATCH 01/10] Simplify feature checking Make mapping features to settings more readable. No functional changes. --- lib/logitech_receiver/settings_templates.py | 27 ++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 3056c9a0..a0ad2e08 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -182,9 +182,24 @@ def check_feature_settings(device, already_known): return if device.protocol and device.protocol < 2.0: return - if not any(s.name == _FN_SWAP[0] for s in already_known) and _F.FN_INVERSION in device.features: - fn_swap = FeatureSettings.fn_swap() - already_known.append(fn_swap(device)) - if not any(s.name == _SMOOTH_SCROLL[0] for s in already_known) and _F.HI_RES_SCROLLING in device.features: - smooth_scroll = FeatureSettings.smooth_scroll() - already_known.append(smooth_scroll(device)) + + def check_feature(name, featureId, field_name=None): + """ + :param name: user-visible setting name. + :param featureId: the numeric Feature ID for this setting. + :param field_name: override the FeatureSettings name if it is + different from the user-visible setting name. Useful if there + are multiple features for the same setting. + """ + if not featureId in device.features: + return + if any(s.name == name for s in already_known): + return + if not field_name: + # Convert user-visible settings name for FeatureSettings + field_name = name.replace('-', '_') + feature = getattr(FeatureSettings, field_name)() + already_known.append(feature(device)) + + check_feature(_SMOOTH_SCROLL[0], _F.HI_RES_SCROLLING) + check_feature(_FN_SWAP[0], _F.FN_INVERSION) From a515cc38605b7bfc533ba32910c6ea7b02c1dad0 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Sun, 31 May 2015 10:56:57 +0200 Subject: [PATCH 02/10] Auto-detect FN swap feature for newer devices --- lib/logitech_receiver/settings_templates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index a0ad2e08..87212c89 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -203,3 +203,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') From 5ba816dd38b87728a640e1de19fddb2d86b99ef1 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Sun, 31 May 2015 11:01:46 +0200 Subject: [PATCH 03/10] [WIP] Support MX Master with DPI adjustment support (#208) It's not known whether the DPI ranges can be queried, so let's set hard-code some values for now. Step size is 200. Does this need to be changed? TODO: need a capture of whether this is really a read function. --- lib/logitech_receiver/descriptors.py | 7 +++++++ lib/logitech_receiver/settings_templates.py | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index 7f85a258..a8d5cc75 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -90,6 +90,7 @@ 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) # # @@ -266,6 +267,12 @@ _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('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002', registers=(_R.battery_status, ), ) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 87212c89..5e20f5a5 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -70,6 +70,16 @@ def feature_toggle(name, feature, rw = _FeatureRW(feature, read_function_id, write_function_id) 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, + label=None, description=None, device_kind=None): + assert choices + validator = _ChoicesV(choices) + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind) + # # common strings for settings # @@ -135,6 +145,15 @@ 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): + """Pointer Speed feature""" + return feature_choices(_DPI[0], _F.ADJUSTABLE_DPI, choices, + # TODO: is this really the read function? + read_function_id=0x20, + write_function_id=0x30, + label=_DPI[1], description=_DPI[2], + device_kind=_DK.mouse) + # # # @@ -165,7 +184,7 @@ FeatureSettings = _SETTINGS_LIST( new_fn_swap=_feature_new_fn_swap, smooth_scroll=_feature_smooth_scroll, side_scroll=None, - dpi=None, + dpi=_feature_adjustable_dpi, hand_detection=None, typing_illumination=None, ) From ab162583e45d89e6350ccc2f77b7a7c812aeb0f6 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Sun, 13 Mar 2016 23:59:21 +0100 Subject: [PATCH 04/10] cli: do not die on missing description The DPI setting has no description, do not try to display it. --- lib/solaar/cli/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py index d26522ec..aed40086 100644 --- a/lib/solaar/cli/config.py +++ b/lib/solaar/cli/config.py @@ -27,7 +27,8 @@ from logitech_receiver import settings as _settings def _print_setting(s, verbose=True): print ('#', s.label) if verbose: - print ('#', s.description.replace('\n', ' ')) + if s.description: + print ('#', s.description.replace('\n', ' ')) if s.kind == _settings.KIND.toggle: print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0') elif s.choices: From 9c768d60a1c7d349a6f396afb72441af79c7d337 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Mon, 14 Mar 2016 00:03:00 +0100 Subject: [PATCH 05/10] 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) From b052ab9ef0b64bbee36e0710ddffc2b3417967ba Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 18 Mar 2016 12:14:15 +0100 Subject: [PATCH 06/10] Fix thinko in Adjustable DPI setting There are three bytes forming the parameter, the sensor ID is the MSB, not LSB. --- lib/logitech_receiver/settings_templates.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index e7f1d052..96a2313f 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -183,14 +183,12 @@ def _feature_adjustable_dpi_choices(device): 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)) + return _NamedInts.list(dpi_list) def _feature_adjustable_dpi(): """Pointer Speed feature""" - # [2] getSensorDpi(sensorIdx) + # Assume sensorIdx 0 (there is only one sensor) + # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB # [3] setSensorDpi(sensorIdx, dpi) return feature_choices_dynamic(_DPI[0], _F.ADJUSTABLE_DPI, _feature_adjustable_dpi_choices, From dd2755909d46982bbc13a428b980037121d88185 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 18 Mar 2016 12:27:16 +0100 Subject: [PATCH 07/10] cli/config: fix error message Attempt to fix: $ bin/solaar config master dpi higher solaar: error: coercing to Unicode: need string or buffer, int found The DPI choices are integers, therefore cast it to a str. --- lib/solaar/cli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py index aed40086..c6bfed06 100644 --- a/lib/solaar/cli/config.py +++ b/lib/solaar/cli/config.py @@ -117,5 +117,5 @@ def run(receivers, args, find_receiver, find_device): result = setting.write(value) if result is None: - raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, value, value)) + raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, str(value), value)) _print_setting(setting, False) From aa7d1b6410d4b1a41400319118623554ce858421 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Thu, 24 Mar 2016 15:13:06 +0100 Subject: [PATCH 08/10] Skip sensorIdx in getSensorDpiList response --- lib/logitech_receiver/settings_templates.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 96a2313f..5aea9ec7 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -26,6 +26,7 @@ from . import hidpp20 as _hidpp20 from .common import ( bytes2int as _bytes2int, NamedInts as _NamedInts, + unpack as _unpack, ) from .settings import ( KIND as _KIND, @@ -171,12 +172,12 @@ def _feature_adjustable_dpi_choices(device): 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]) + for val in _unpack('!B7H', reply)[1:]: if val == 0: break if val >> 13 == 0b111: - assert offset == 2, 'Invalid DPI list item: %r' % val + assert step is None and len(dpi_list) == 1, \ + 'Invalid DPI list item: %r' % val step = val & 0x1fff else: dpi_list.append(val) From d1858f747bcb69d841e74bf698e61af901670391 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Thu, 24 Mar 2016 16:59:05 +0100 Subject: [PATCH 09/10] Assume 7 words for the DPI list response HID++ 2.0 responses are 20 bytes, once you strip the 4 byte common header and 1 byte sensorIdx, you have 15 bytes left. Since DPI values are 16-bit words, only 14 bytes should be used. --- lib/logitech_receiver/settings_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 5aea9ec7..4123e7d1 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -172,7 +172,7 @@ def _feature_adjustable_dpi_choices(device): assert reply, 'Oops, DPI list cannot be retrieved!' dpi_list = [] step = None - for val in _unpack('!B7H', reply)[1:]: + for val in _unpack('!7H', reply[1:1+14]): if val == 0: break if val >> 13 == 0b111: From 883ed9549d4e4488fcefb7f64efd3948f472e52c Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 25 Mar 2016 00:06:00 +0100 Subject: [PATCH 10/10] Fix DPI list assertion --- lib/logitech_receiver/settings_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 4123e7d1..a2df4a29 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -182,7 +182,7 @@ def _feature_adjustable_dpi_choices(device): else: dpi_list.append(val) if step: - assert dpi_list == 2, 'Invalid DPI list range: %r' % dpi_list + assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) return _NamedInts.list(dpi_list)