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)
|
# assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed)
|
||||||
self._fallback = None
|
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
|
@classmethod
|
||||||
def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):
|
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)}
|
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',
|
_D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002',
|
||||||
registers=(_R.battery_status, ),
|
registers=(_R.battery_status, ),
|
||||||
)
|
)
|
||||||
|
|
|
@ -317,7 +317,11 @@ class ChoicesValidator(object):
|
||||||
|
|
||||||
kind = KIND.choice
|
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 choices is not None
|
||||||
assert isinstance(choices, _NamedInts)
|
assert isinstance(choices, _NamedInts)
|
||||||
assert len(choices) > 2
|
assert len(choices) > 2
|
||||||
|
@ -326,6 +330,9 @@ class ChoicesValidator(object):
|
||||||
|
|
||||||
max_bits = max(x.bit_length() for x in choices)
|
max_bits = max(x.bit_length() for x in choices)
|
||||||
self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
|
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
|
assert self._bytes_count < 8
|
||||||
|
|
||||||
def validate_read(self, reply_bytes):
|
def validate_read(self, reply_bytes):
|
||||||
|
|
|
@ -23,6 +23,11 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
from . import hidpp10 as _hidpp10
|
from . import hidpp10 as _hidpp10
|
||||||
from . import hidpp20 as _hidpp20
|
from . import hidpp20 as _hidpp20
|
||||||
|
from .common import (
|
||||||
|
bytes2int as _bytes2int,
|
||||||
|
NamedInts as _NamedInts,
|
||||||
|
unpack as _unpack,
|
||||||
|
)
|
||||||
from .settings import (
|
from .settings import (
|
||||||
KIND as _KIND,
|
KIND as _KIND,
|
||||||
Setting as _Setting,
|
Setting as _Setting,
|
||||||
|
@ -70,6 +75,30 @@ def feature_toggle(name, feature,
|
||||||
rw = _FeatureRW(feature, read_function_id, write_function_id)
|
rw = _FeatureRW(feature, read_function_id, write_function_id)
|
||||||
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
|
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
|
# common strings for settings
|
||||||
#
|
#
|
||||||
|
@ -135,6 +164,41 @@ def _feature_smooth_scroll():
|
||||||
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
|
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
|
||||||
device_kind=_DK.mouse)
|
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,
|
new_fn_swap=_feature_new_fn_swap,
|
||||||
smooth_scroll=_feature_smooth_scroll,
|
smooth_scroll=_feature_smooth_scroll,
|
||||||
side_scroll=None,
|
side_scroll=None,
|
||||||
dpi=None,
|
dpi=_feature_adjustable_dpi,
|
||||||
hand_detection=None,
|
hand_detection=None,
|
||||||
typing_illumination=None,
|
typing_illumination=None,
|
||||||
)
|
)
|
||||||
|
@ -182,9 +246,26 @@ def check_feature_settings(device, already_known):
|
||||||
return
|
return
|
||||||
if device.protocol and device.protocol < 2.0:
|
if device.protocol and device.protocol < 2.0:
|
||||||
return
|
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()
|
def check_feature(name, featureId, field_name=None):
|
||||||
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:
|
:param name: user-visible setting name.
|
||||||
smooth_scroll = FeatureSettings.smooth_scroll()
|
:param featureId: the numeric Feature ID for this setting.
|
||||||
already_known.append(smooth_scroll(device))
|
: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):
|
def _print_setting(s, verbose=True):
|
||||||
print ('#', s.label)
|
print ('#', s.label)
|
||||||
if verbose:
|
if verbose:
|
||||||
print ('#', s.description.replace('\n', ' '))
|
if s.description:
|
||||||
|
print ('#', s.description.replace('\n', ' '))
|
||||||
if s.kind == _settings.KIND.toggle:
|
if s.kind == _settings.KIND.toggle:
|
||||||
print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
|
print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
|
||||||
elif s.choices:
|
elif s.choices:
|
||||||
|
@ -116,5 +117,5 @@ def run(receivers, args, find_receiver, find_device):
|
||||||
|
|
||||||
result = setting.write(value)
|
result = setting.write(value)
|
||||||
if result is None:
|
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)
|
_print_setting(setting, False)
|
||||||
|
|
Loading…
Reference in New Issue