Merge branch 'features'

Automatically detect FN swap feature and DPI adjustment on some newer
devices. DPI adjustment partially addresses support for the MX Master
(#208), Smart shift is still missing.
This commit is contained in:
Peter Wu 2016-04-17 12:43:15 +02:00
commit 2041007b38
5 changed files with 106 additions and 10 deletions

View File

@ -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)}

View File

@ -271,6 +271,8 @@ _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')
_D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002',
registers=(_R.battery_status, ),
)

View File

@ -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):

View File

@ -23,6 +23,11 @@ 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,
unpack as _unpack,
)
from .settings import (
KIND as _KIND,
Setting as _Setting,
@ -70,6 +75,30 @@ 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, write_function_id,
bytes_count=None,
label=None, description=None, device_kind=None):
assert choices
validator = _ChoicesV(choices, bytes_count=bytes_count)
rw = _FeatureRW(feature, read_function_id, write_function_id)
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
#
@ -135,6 +164,41 @@ def _feature_smooth_scroll():
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
device_kind=_DK.mouse)
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 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, \
'Invalid DPI list item: %r' % val
step = val & 0x1fff
else:
dpi_list.append(val)
if step:
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)
def _feature_adjustable_dpi():
"""Pointer Speed feature"""
# 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,
read_function_id=0x20,
write_function_id=0x30,
bytes_count=3,
label=_DPI[1], description=_DPI[2],
device_kind=_DK.mouse)
#
#
#
@ -165,7 +229,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,
)
@ -182,9 +246,26 @@ 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)
check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap')
check_feature(_DPI[0], _F.ADJUSTABLE_DPI)

View File

@ -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:
@ -116,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)