From f0c5046ccfcf327b84d29e20aa3e73bdd0acd548 Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Fri, 14 Dec 2012 06:44:44 +0200 Subject: [PATCH] re-worked the settings classes --- lib/logitech/unifying_receiver/common.py | 5 + lib/logitech/unifying_receiver/descriptors.py | 48 ++++- lib/logitech/unifying_receiver/hidpp10.py | 63 +----- lib/logitech/unifying_receiver/hidpp20.py | 28 --- lib/logitech/unifying_receiver/receiver.py | 20 +- lib/logitech/unifying_receiver/settings.py | 193 ++++++++++++++++-- 6 files changed, 231 insertions(+), 126 deletions(-) diff --git a/lib/logitech/unifying_receiver/common.py b/lib/logitech/unifying_receiver/common.py index 7346f75f..87208a1a 100644 --- a/lib/logitech/unifying_receiver/common.py +++ b/lib/logitech/unifying_receiver/common.py @@ -72,6 +72,11 @@ class NamedInts(object): self._indexed = {int(v): v for v in self._values} self._fallback = None + @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)} + return NamedInts(**values) + def flag_names(self, value): unknown_bits = value for k in self._indexed: diff --git a/lib/logitech/unifying_receiver/descriptors.py b/lib/logitech/unifying_receiver/descriptors.py index 57823594..96be4e2f 100644 --- a/lib/logitech/unifying_receiver/descriptors.py +++ b/lib/logitech/unifying_receiver/descriptors.py @@ -7,7 +7,38 @@ from __future__ import absolute_import, division, print_function, unicode_litera from collections import namedtuple from .common import NamedInts as _NamedInts -from . import hidpp10 +from . import hidpp10 as _hidpp10 +from . import hidpp20 as _hidpp20 +from . import settings as _settings + +# +# common strings for settings +# + +_SMOOTH_SCROLL = ('smooth-scroll', 'Smooth Scrolling', 'High-sensitivity mode for vertical scroll with the wheel.') +_DPI = ('dpi', 'Sensitivity (DPI)', None) +_FN_SWAP = ('fn-swap', 'Swap Fx function', ('When set, the F1..F12 keys will activate their special function,\n' + 'and you must hold the FN key to activate their standard function.\n' + '\n' + 'When unset, the F1..F12 keys will activate their standard function,\n' + 'and you must hold the FN key to activate their special function.')) + + +def _register_smooth_scroll(register, true_value, mask): + return _settings.register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask, + label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2]) + + +def _register_dpi(register, choices): + return _settings.register_choices(_DPI[0], register, choices, + label=_DPI[1], description=_DPI[2]) + + +def check_features(device, already_known): + if _hidpp20.FEATURE.FN_STATUS in device.features and not any(s.name == 'fn-swap' for s in already_known): + tfn = _settings.feature_toggle(_FN_SWAP[0], _hidpp20.FEATURE.FN_STATUS, write_returns_value=True, + label=_FN_SWAP[1], description=_FN_SWAP[2]) + already_known.append(tfn(device)) # # @@ -20,10 +51,10 @@ DEVICES = {} def _D(name, codename=None, kind=None, registers=None, settings=None): if kind is None: - kind = (hidpp10.DEVICE_KIND.mouse if 'Mouse' in name - else hidpp10.DEVICE_KIND.keyboard if 'Keyboard' in name - else hidpp10.DEVICE_KIND.touchpad if 'Touchpad' in name - else hidpp10.DEVICE_KIND.trackball if 'Trackball' in name + kind = (_hidpp10.DEVICE_KIND.mouse if 'Mouse' in name + else _hidpp10.DEVICE_KIND.keyboard if 'Keyboard' in name + else _hidpp10.DEVICE_KIND.touchpad if 'Touchpad' in name + else _hidpp10.DEVICE_KIND.trackball if 'Trackball' in name else None) assert kind is not None @@ -43,7 +74,10 @@ _D('Wireless Mouse M525') _D('Wireless Trackball M570') _D('Touch Mouse M600') _D('Marathon Mouse M705', - settings=[hidpp10.SmoothScroll_Setting(0x01)], + settings=[ + _register_smooth_scroll(0x01, true_value=0x40, mask=0x40), + _register_dpi(0x63, _NamedInts.range(9, 11, lambda x: '_' + str(x * 100))), + ], ) _D('Wireless Keyboard K230') _D('Wireless Keyboard K270') @@ -58,7 +92,7 @@ _D('Logitech Cube', kind='mouse') _D('Anywhere Mouse MX', codename='Anywhere MX') _D('Performance Mouse MX', codename='Performance MX', settings=[ - hidpp10.MouseDPI_Setting(0x63, _NamedInts(**{str(x * 100): (0x80 + x) for x in range(1, 16)})), + _register_dpi(0x63, _NamedInts.range(0x81, 0x8F, lambda x: '_' + str((x - 0x80) * 100))), ], ) diff --git a/lib/logitech/unifying_receiver/hidpp10.py b/lib/logitech/unifying_receiver/hidpp10.py index f73c0129..1fd0f886 100644 --- a/lib/logitech/unifying_receiver/hidpp10.py +++ b/lib/logitech/unifying_receiver/hidpp10.py @@ -4,15 +4,13 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from logging import getLogger, DEBUG as _DEBUG +from logging import getLogger # , DEBUG as _DEBUG _log = getLogger('LUR').getChild('hidpp10') del getLogger from .common import (strhex as _strhex, NamedInts as _NamedInts, - NamedInt as _NamedInt, FirmwareInfo as _FirmwareInfo) -from . import settings as _settings from .hidpp20 import FIRMWARE_KIND # @@ -65,65 +63,6 @@ PAIRING_ERRORS = _NamedInts( too_many_devices=0x03, sequence_timeout=0x06) -# -# -# - - -class SmoothScroll_Setting(_settings.Setting): - def __init__(self, register): - super(SmoothScroll_Setting, self).__init__('smooth-scroll', _settings.KIND.toggle, - 'Smooth Scrolling', 'High-sensitivity mode for vertical scroll with the wheel.') - assert register is not None - self.register = register - - def read(self, cached=True): - if (self._value is None or not cached) and self._device: - ss = self.read_register() - if ss: - self._value = (ss[:1] == b'\x40') - return self._value - - def write(self, value): - if self._device: - reply = self.write_register(0x40 if bool(value) else 0x00) - # self._value = None - if reply: - self._value = value - return value - # return self.read(False) - - -class MouseDPI_Setting(_settings.Setting): - def __init__(self, register, choices): - super(MouseDPI_Setting, self).__init__('dpi', _settings.KIND.choice, - 'Sensitivity (DPI)', choices=choices) - assert choices - assert isinstance(choices, _NamedInts) - assert register is not None - self.register = register - - def read(self, cached=True): - if (self._value is None or not cached) and self._device: - dpi = self.read_register() - if dpi: - value = ord(dpi[:1]) - self._value = self.choices[value] - assert self._value is not None - return self._value - - def write(self, value): - if self._device: - choice = self.choices[value] - if choice is None: - raise ValueError(repr(value)) - reply = self.write_register(value) - # self._value = None - if reply: - self._value = value - return value - # return self.read(False) - # # functions # diff --git a/lib/logitech/unifying_receiver/hidpp20.py b/lib/logitech/unifying_receiver/hidpp20.py index 4803ce7a..eb5ae053 100644 --- a/lib/logitech/unifying_receiver/hidpp20.py +++ b/lib/logitech/unifying_receiver/hidpp20.py @@ -11,7 +11,6 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger('LUR').getChild('hidpp20') del getLogger -from . import settings as _settings from .common import (FirmwareInfo as _FirmwareInfo, ReprogrammableKeyInfo as _ReprogrammableKeyInfo, KwException as _KwException, @@ -300,33 +299,6 @@ class KeysArray(object): def __len__(self): return len(self.keys) - -# -# -# - -class ToggleFN_Setting(_settings.Setting): - def __init__(self): - super(ToggleFN_Setting, self).__init__('fn-swap', _settings.KIND.toggle, 'Swap Fx function', - 'When set, the F1..F12 keys will activate their special function,\n' - 'and you must hold the FN key to activate their standard function.\n' - '\n' - 'When unset, the F1..F12 keys will activate their standard function,\n' - 'and you must hold the FN key to activate their special function.') - - def read(self, cached=True): - if (self._value is None or not cached) and self._device: - fn = self._device.feature_request(FEATURE.FN_STATUS) - if fn: - self._value = (fn[:1] == b'\x01') - return self._value - - def write(self, value): - if self._device: - reply = self._device.feature_request(FEATURE.FN_STATUS, 0x10, 0x01 if value else 0x00) - self._value = (reply[:1] == b'\x01') if reply else None - return self._value - # # # diff --git a/lib/logitech/unifying_receiver/receiver.py b/lib/logitech/unifying_receiver/receiver.py index 92da6c83..9147c801 100644 --- a/lib/logitech/unifying_receiver/receiver.py +++ b/lib/logitech/unifying_receiver/receiver.py @@ -15,7 +15,7 @@ from . import base as _base from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import strhex as _strhex, NamedInts as _NamedInts -from .descriptors import DEVICES as _DEVICES +from . import descriptors as _descriptors # # @@ -94,8 +94,8 @@ class PairedDevice(object): @property def name(self): if self._name is None: - if self.codename in _DEVICES: - self._name, self._kind = _DEVICES[self._codename][:2] + if self.codename in _descriptors.DEVICES: + self._name, self._kind = _descriptors.DEVICES[self._codename][:2] elif self.protocol >= 2.0: self._name = _hidpp20.get_name(self) return self._name or self.codename or '?' @@ -110,8 +110,8 @@ class PairedDevice(object): if self._wpid is None: self._wpid = _strhex(pair_info[3:5]) if self._kind is None: - if self.codename in _DEVICES: - self._name, self._kind = _DEVICES[self._codename][:2] + if self.codename in _descriptors.DEVICES: + self._name, self._kind = _descriptors.DEVICES[self._codename][:2] elif self.protocol >= 2.0: self._kind = _hidpp20.get_kind(self) return self._kind or '?' @@ -141,7 +141,7 @@ class PairedDevice(object): @property def registers(self): if self._registers is None: - descriptor = _DEVICES.get(self.codename) + descriptor = _descriptors.DEVICES.get(self.codename) if descriptor is None or descriptor.registers is None: self._registers = _NamedInts() else: @@ -151,16 +151,14 @@ class PairedDevice(object): @property def settings(self): if self._settings is None: - descriptor = _DEVICES.get(self.codename) + descriptor = _descriptors.DEVICES.get(self.codename) if descriptor is None or descriptor.settings is None: self._settings = [] else: self._settings = [s(self) for s in descriptor.settings] - if _hidpp20.FEATURE.FN_STATUS in self.features: - tfn = _hidpp20.ToggleFN_Setting() - self._settings.insert(0, tfn(self)) - + if self.features: + _descriptors.check_features(self, self._settings) return self._settings def request(self, request_id, *params): diff --git a/lib/logitech/unifying_receiver/settings.py b/lib/logitech/unifying_receiver/settings.py index 2f816901..11519c6c 100644 --- a/lib/logitech/unifying_receiver/settings.py +++ b/lib/logitech/unifying_receiver/settings.py @@ -7,24 +7,30 @@ from __future__ import absolute_import, division, print_function, unicode_litera from weakref import proxy as _proxy from copy import copy as _copy -from .common import NamedInts as _NamedInts +from .common import NamedInt as _NamedInt, NamedInts as _NamedInts # # # -KIND = _NamedInts(toggle=0x1, choice=0x02, range=0x03) +KIND = _NamedInts(toggle=0x1, choice=0x02, range=0x12) -class Setting(object): - __slots__ = ['name', 'kind', 'label', 'description', 'choices', '_device', '_value', 'register'] +class _Setting(object): + __slots__ = ['name', 'label', 'description', + 'kind', '_rw', '_validator', + '_device', '_value'] - def __init__(self, name, kind, label, description=None, choices=None): + def __init__(self, name, rw, validator, kind=None, label=None, description=None): + assert name self.name = name - self.kind = kind - self.label = label + self.label = label or name self.description = description - self.choices = choices - self.register = None + + self._rw = rw + self._validator = validator + + assert kind is None or kind & validator.kind != 0 + self.kind = kind or validator.kind def __call__(self, device): o = _copy(self) @@ -32,21 +38,172 @@ class Setting(object): o._device = _proxy(device) return o - def read_register(self): - return self._device.request(0x8100 | (self.register & 0x2FF)) - - def write_register(self, value, value2=0): - return self._device.request(0x8000 | (self.register & 0x2FF), int(value) & 0xFF, int(value2) & 0xFF) + @property + def choices(self): + return self._validator.choices if self._validator.kind & KIND.choice else None def read(self, cached=True): - raise NotImplemented + if self._device: + if self._value is None or not cached: + reply = self._rw.read(self._device) + # print ("read reply", repr(reply)) + if reply: + # print ("pre-read", self._value) + self._value = self._validator.validate_read(reply) + # print ("post-read", self._value) + return self._value def write(self, value): - raise NotImplemented + if self._device: + data_bytes = self._validator.prepare_write(value) + reply = self._rw.write(self._device, data_bytes) + if reply: + self._value = self._validator.validate_write(value, reply) + return self._value def __str__(self): if hasattr(self, '_value'): assert hasattr(self, '_device') - return'<%s(%s:%s=%s)>' % (self.__class__.__name__, self._device.codename, self.name, self._value) - return '<%s(%s)>' % (self.__class__.__name__, self.name) + return '' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value) + return '' % (self._rw.kind, self._validator.kind, self.name) __unicode__ = __repr__ = __str__ + + +class _RegisterRW(object): + __slots__ = ['register'] + + kind = _NamedInt(0x01, 'register') + + def __init__(self, register): + assert isinstance(register, int) + self.register = register + + def read(self, device): + return device.request(0x8100 | (self.register & 0x2FF)) + + def write(self, device, data_bytes): + return device.request(0x8000 | (self.register & 0x2FF), data_bytes) + + +class _FeatureRW(object): + __slots__ = ['feature', 'read_fnid', 'write_fnid'] + + kind = _NamedInt(0x02, 'feature') + default_read_fnid = 0x00 + default_write_fnid = 0x10 + + def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid): + assert isinstance(feature, _NamedInt) + self.feature = feature + self.read_fnid = read_fnid + self.write_fnid = write_fnid + + def read(self, device): + assert self.feature is not None + return device.feature_request(self.feature, self.read_fnid) + + def write(self, device, data_bytes): + assert self.feature is not None + return device.feature_request(self.feature, self.write_fnid, data_bytes) + + +class _BooleanValidator(object): + __slots__ = ['true_value', 'false_value', 'mask', 'write_returns_value'] + + kind = KIND.toggle + default_true = 0x01 + default_false = 0x00 + default_mask = 0xFF + + def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask, write_returns_value=False): + self.true_value = true_value + self.false_value = false_value + self.mask = mask + self.write_returns_value = write_returns_value + + def validate_read(self, reply_bytes): + reply_value = ord(reply_bytes[:1]) & self.mask + return reply_value == self.true_value + + def prepare_write(self, value): + # FIXME: this does not work right when there is more than one flag in + # the same register! + return self.true_value if value else self.false_value + + def validate_write(self, value, reply_bytes): + if self.write_returns_value: + reply_value = ord(reply_bytes[:1]) & self.mask + return reply_value == self.true_value + + # just assume the value was written correctly, otherwise there would not + # be any reply_bytes to check + return bool(value) + + +class _ChoicesValidator(object): + __slots__ = ['choices', 'write_returns_value'] + + kind = KIND.choice + + def __init__(self, choices, write_returns_value=False): + assert isinstance(choices, _NamedInts) + self.choices = choices + self.write_returns_value = write_returns_value + + def validate_read(self, reply_bytes): + assert self.choices is not None + reply_value = ord(reply_bytes[:1]) + valid_value = self.choices[reply_value] + assert valid_value is not None, "%: failed to validate read value %02X" % (self.__class__.__name__, reply_value) + return valid_value + + def prepare_write(self, value): + assert self.choices is not None + choice = self.choices[value] + if choice is None: + raise ValueError("invalid choice " + repr(value)) + assert isinstance(choice, _NamedInt) + return choice.bytes(1) + + def validate_write(self, value, reply_bytes): + assert self.choices is not None + if self.write_returns_value: + reply_value = ord(reply_bytes[:1]) + choice = self.choices[reply_value] + assert choice is not None, "failed to validate write reply %02X" % reply_value + return choice + + # just assume the value was written correctly, otherwise there would not + # be any reply_bytes to check + return self.choices[value] + +# +# +# + +def register_toggle(name, register, + true_value=_BooleanValidator.default_true, false_value=_BooleanValidator.default_false, + mask=_BooleanValidator.default_mask, write_returns_value=False, + label=None, description=None): + rw = _RegisterRW(register) + validator = _BooleanValidator(true_value=true_value, false_value=false_value, mask=mask, write_returns_value=write_returns_value) + return _Setting(name, rw, validator, label=label, description=description) + + +def register_choices(name, register, choices, + kind=KIND.choice, write_returns_value=False, + label=None, description=None): + assert choices + rw = _RegisterRW(register) + validator = _ChoicesValidator(choices, write_returns_value=write_returns_value) + return _Setting(name, rw, validator, kind=kind, label=label, description=description) + + +def feature_toggle(name, feature, + read_function_id=_FeatureRW.default_read_fnid, write_function_id=_FeatureRW.default_write_fnid, + true_value=_BooleanValidator.default_true, false_value=_BooleanValidator.default_false, + mask=_BooleanValidator.default_mask, write_returns_value=False, + label=None, description=None): + rw = _FeatureRW(feature, read_function_id, write_function_id) + validator = _BooleanValidator(true_value=true_value, false_value=false_value, mask=mask, write_returns_value=write_returns_value) + return _Setting(name, rw, validator, label=label, description=description)