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.
This commit is contained in:
Peter Wu 2016-03-14 00:03:00 +01:00
parent ab162583e4
commit 9c768d60a1
4 changed files with 69 additions and 15 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

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

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,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)