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:
commit
2041007b38
|
@ -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)}
|
||||
|
|
|
@ -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, ),
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue